From 33239700fdb0e7ce72c37baa0db8927dea5bb1fb Mon Sep 17 00:00:00 2001 From: dk <1260500659@qq.com> Date: Thu, 11 Jun 2026 09:58:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=8A=A0=E7=8F=AD=E7=94=B3=E8=AF=B7):=20?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E6=92=A4=E9=94=80=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=92=8C=E5=8A=A8=E4=BD=9C=E3=80=82=20feat(?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=8A=A5=E5=91=8A):=20=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=8A=A5=E5=91=8A=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E5=8C=85=E5=90=AB=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/enums/ErrorCodeConstants.java | 18 +- rdms-project/rdms-project-boot/pom.xml | 6 + .../OvertimeApplicationConstants.java | 2 - .../project/constant/WorkReportConstants.java | 41 + .../OvertimeApplicationController.java | 21 +- ...ertimeApplicationApprovalRecordRespVO.java | 29 + .../OvertimeApplicationStatusActionReqVO.java | 2 +- .../common/WorkReportExportResponseUtils.java | 20 + .../common/WorkReportStatusController.java | 35 + .../vo/PersonalReportPlanItemReqVO.java | 19 + .../vo/PersonalReportPlanItemRespVO.java | 21 + .../vo/PersonalReportReviewItemReqVO.java | 23 + .../vo/PersonalReportReviewItemRespVO.java | 25 + .../vo/WorkReportApprovalRecordRespVO.java | 27 + .../common/vo/WorkReportBasePageReqVO.java | 32 + .../vo/WorkReportMemberSnapshotRespVO.java | 13 + .../vo/WorkReportStatusActionReqVO.java | 14 + .../common/vo/WorkReportStatusDictRespVO.java | 27 + .../common/vo/WorkReportStatusLogRespVO.java | 35 + .../monthly/MonthlyReportController.java | 172 ++ .../vo/MonthlyReportApprovalRecordRespVO.java | 50 + .../monthly/vo/MonthlyReportApproveReqVO.java | 57 + .../vo/MonthlyReportContentExportReqVO.java | 19 + .../vo/MonthlyReportDefaultDraftReqVO.java | 30 + .../monthly/vo/MonthlyReportExportVO.java | 44 + .../monthly/vo/MonthlyReportPageReqVO.java | 12 + .../monthly/vo/MonthlyReportRespVO.java | 41 + .../monthly/vo/MonthlyReportSaveReqVO.java | 40 + .../project/ProjectReportController.java | 181 ++ .../vo/ProjectReportContentExportReqVO.java | 19 + .../vo/ProjectReportDefaultDraftReqVO.java | 33 + .../project/vo/ProjectReportExportVO.java | 47 + .../project/vo/ProjectReportItemReqVO.java | 19 + .../project/vo/ProjectReportItemRespVO.java | 21 + ...ProjectReportOwnerProjectOptionRespVO.java | 18 + .../project/vo/ProjectReportPageReqVO.java | 18 + .../project/vo/ProjectReportRespVO.java | 47 + .../project/vo/ProjectReportSaveReqVO.java | 52 + .../weekly/WeeklyReportController.java | 171 ++ .../vo/WeeklyReportContentExportReqVO.java | 19 + .../vo/WeeklyReportDefaultDraftReqVO.java | 30 + .../weekly/vo/WeeklyReportExportVO.java | 50 + .../weekly/vo/WeeklyReportPageReqVO.java | 15 + .../weekly/vo/WeeklyReportRespVO.java | 44 + .../weekly/vo/WeeklyReportSaveReqVO.java | 46 + .../vo/WeeklyReportTravelSegmentReqVO.java | 27 + .../vo/WeeklyReportTravelSegmentRespVO.java | 24 + .../OvertimeApplicationApprovalRecordDO.java | 36 + .../common/PersonalReportPlanItemDO.java | 40 + .../common/PersonalReportReviewItemDO.java | 45 + .../common/WorkReportMemberSnapshotItem.java | 14 + .../common/WorkReportStatusLogDO.java | 44 + .../MonthlyReportApprovalRecordDO.java | 71 + .../workreport/monthly/MonthlyReportDO.java | 64 + .../ProjectReportApprovalRecordDO.java | 36 + .../project/ProjectReportCurrentItemDO.java | 36 + .../workreport/project/ProjectReportDO.java | 85 + .../project/ProjectReportNextItemDO.java | 33 + .../weekly/WeeklyReportApprovalRecordDO.java | 36 + .../workreport/weekly/WeeklyReportDO.java | 69 + .../weekly/WeeklyReportTravelSegmentDO.java | 36 + ...ertimeApplicationApprovalRecordMapper.java | 24 + .../mysql/personal/PersonalItemMapper.java | 32 + .../dal/mysql/project/ProjectMapper.java | 6 + .../execution/ProjectExecutionMapper.java | 54 +- .../mysql/project/task/ProjectTaskMapper.java | 68 + .../mysql/project/task/TaskWorklogMapper.java | 26 + .../common/PersonalReportPlanItemMapper.java | 26 + .../PersonalReportReviewItemMapper.java | 26 + .../common/WorkReportStatusLogMapper.java | 26 + .../MonthlyReportApprovalRecordMapper.java | 29 + .../monthly/MonthlyReportMapper.java | 123 ++ .../ProjectReportApprovalRecordMapper.java | 29 + .../ProjectReportCurrentItemMapper.java | 23 + .../project/ProjectReportMapper.java | 136 ++ .../project/ProjectReportNextItemMapper.java | 23 + .../WeeklyReportApprovalRecordMapper.java | 29 + .../workreport/weekly/WeeklyReportMapper.java | 124 ++ .../WeeklyReportTravelSegmentMapper.java | 24 + .../rpc/config/RpcConfiguration.java | 9 +- .../overtime/OvertimeApplicationService.java | 5 +- .../OvertimeApplicationServiceImpl.java | 71 +- .../common/WorkReportCommonService.java | 1354 ++++++++++++++ .../common/WorkReportStatusService.java | 10 + .../common/WorkReportStatusServiceImpl.java | 19 + .../WorkReportDefaultDraftService.java | 930 ++++++++++ .../WorkReportContentExportService.java | 1583 +++++++++++++++++ .../export/WorkReportExportFile.java | 4 + .../monthly/MonthlyReportService.java | 45 + .../monthly/MonthlyReportServiceImpl.java | 97 + .../project/ProjectReportService.java | 47 + .../project/ProjectReportServiceImpl.java | 106 ++ .../weekly/WeeklyReportService.java | 44 + .../weekly/WeeklyReportServiceImpl.java | 96 + .../work-report/monthly-report-template.docx | Bin 0 -> 25281 bytes .../work-report/project-report-template.docx | Bin 0 -> 15071 bytes .../work-report/weekly-report-template.docx | Bin 0 -> 24346 bytes 97 files changed, 7581 insertions(+), 68 deletions(-) create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/WorkReportConstants.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationApprovalRecordRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/WorkReportExportResponseUtils.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/WorkReportStatusController.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportPlanItemReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportPlanItemRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportReviewItemReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportReviewItemRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportApprovalRecordRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportBasePageReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportMemberSnapshotRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusActionReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusDictRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusLogRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/MonthlyReportController.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApprovalRecordRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApproveReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportContentExportReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportDefaultDraftReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportExportVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportPageReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSaveReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/ProjectReportController.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportContentExportReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportDefaultDraftReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportExportVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportItemReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportItemRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportOwnerProjectOptionRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportPageReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportSaveReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/WeeklyReportController.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportContentExportReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportDefaultDraftReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportExportVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportPageReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportSaveReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportTravelSegmentReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportTravelSegmentRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/overtime/OvertimeApplicationApprovalRecordDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/PersonalReportPlanItemDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/PersonalReportReviewItemDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/WorkReportMemberSnapshotItem.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/WorkReportStatusLogDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportApprovalRecordDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportApprovalRecordDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportCurrentItemDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportNextItemDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportApprovalRecordDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportTravelSegmentDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/overtime/OvertimeApplicationApprovalRecordMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/PersonalReportPlanItemMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/PersonalReportReviewItemMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/WorkReportStatusLogMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportApprovalRecordMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportApprovalRecordMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportCurrentItemMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportNextItemMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportApprovalRecordMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportTravelSegmentMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportStatusService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportStatusServiceImpl.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/defaultdraft/WorkReportDefaultDraftService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportContentExportService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportExportFile.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportServiceImpl.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportServiceImpl.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportServiceImpl.java create mode 100644 rdms-project/rdms-project-boot/src/main/resources/templates/work-report/monthly-report-template.docx create mode 100644 rdms-project/rdms-project-boot/src/main/resources/templates/work-report/project-report-template.docx create mode 100644 rdms-project/rdms-project-boot/src/main/resources/templates/work-report/weekly-report-template.docx diff --git a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java index e92172f..60b3096 100644 --- a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java +++ b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java @@ -240,5 +240,21 @@ public interface ErrorCodeConstants { ErrorCode OVERTIME_APPLICATION_APPROVER_INVALID = new ErrorCode(1_008_009_008, "审核人不是有效系统用户"); ErrorCode OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN = new ErrorCode(1_008_009_009, "审核人不能选择申请人本人"); ErrorCode OVERTIME_APPLICATION_READ_FORBIDDEN = new ErrorCode(1_008_009_010, "无权查看该加班申请"); - ErrorCode OVERTIME_APPLICATION_DELETE_ONLY_CANCELLED = new ErrorCode(1_008_009_011, "仅已撤销的加班申请允许删除"); + ErrorCode OVERTIME_APPLICATION_DELETE_ONLY_REJECTED = new ErrorCode(1_008_009_011, "仅已退回的加班申请允许删除"); + // ========== 工作报告 1_008_010_xxx ========== + ErrorCode WORK_REPORT_NOT_EXISTS = new ErrorCode(1_008_010_001, "工作报告不存在"); + ErrorCode WORK_REPORT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_010_002, "工作报告状态定义不存在或已停用"); + ErrorCode WORK_REPORT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_010_003, "当前工作报告状态不支持动作【{}】"); + ErrorCode WORK_REPORT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_010_004, "动作【{}】必须填写原因"); + ErrorCode WORK_REPORT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_010_005, "工作报告状态已发生变化,请刷新后重试"); + ErrorCode WORK_REPORT_REPORTER_ONLY = new ErrorCode(1_008_010_006, "仅填报人可执行该操作"); + ErrorCode WORK_REPORT_APPROVER_ONLY = new ErrorCode(1_008_010_007, "仅当前审核人可执行该操作"); + ErrorCode WORK_REPORT_READ_FORBIDDEN = new ErrorCode(1_008_010_008, "无权查看该工作报告"); + ErrorCode WORK_REPORT_DIRECT_MANAGER_NOT_EXISTS = new ErrorCode(1_008_010_009, "当前用户不存在生效中的直属上级,无法提交工作报告"); + ErrorCode WORK_REPORT_PROJECT_NOT_EXISTS = new ErrorCode(1_008_010_010, "项目半月报对应的项目不存在"); + ErrorCode WORK_REPORT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_010_011, "当前工作报告状态不允许编辑"); + ErrorCode WORK_REPORT_DELETE_NOT_ALLOWED = new ErrorCode(1_008_010_012, "当前工作报告状态不允许删除"); + ErrorCode WORK_REPORT_PERIOD_DUPLICATE = new ErrorCode(1_008_010_013, "当前周期的工作报告已存在,请勿重复创建"); + ErrorCode WORK_REPORT_APPROVAL_RECORD_NOT_EXISTS = new ErrorCode(1_008_010_014, "工作报告审核记录不存在"); + ErrorCode WORK_REPORT_PROJECT_OWNER_ONLY = new ErrorCode(1_008_010_015, "仅项目负责人可创建或维护该项目的项目半月报"); } diff --git a/rdms-project/rdms-project-boot/pom.xml b/rdms-project/rdms-project-boot/pom.xml index b075fca..e24d952 100644 --- a/rdms-project/rdms-project-boot/pom.xml +++ b/rdms-project/rdms-project-boot/pom.xml @@ -65,6 +65,12 @@ rdms-spring-boot-starter-excel + + org.apache.poi + poi-ooxml + 5.4.1 + + com.alibaba.cloud diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/OvertimeApplicationConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/OvertimeApplicationConstants.java index 04b4371..de05c72 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/OvertimeApplicationConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/OvertimeApplicationConstants.java @@ -14,7 +14,6 @@ public final class OvertimeApplicationConstants { public static final String STATUS_PENDING = "pending"; public static final String STATUS_APPROVED = "approved"; public static final String STATUS_REJECTED = "rejected"; - public static final String STATUS_CANCELLED = "cancelled"; /** * 新建即提交的业务日志动作。 @@ -25,7 +24,6 @@ public final class OvertimeApplicationConstants { public static final String ACTION_RESUBMIT = "resubmit"; public static final String ACTION_APPROVE = "approve"; public static final String ACTION_REJECT = "reject"; - public static final String ACTION_CANCEL = "cancel"; public static final String ACTION_DELETE = "delete"; public static final String PERMISSION_QUERY = "project:overtime-application:query"; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/WorkReportConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/WorkReportConstants.java new file mode 100644 index 0000000..f4c3772 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/WorkReportConstants.java @@ -0,0 +1,41 @@ +package com.njcn.rdms.module.project.constant; + +/** + * 工作报告常量。 + */ +public final class WorkReportConstants { + + private WorkReportConstants() { + } + + public static final String STATUS_OBJECT_TYPE = "work_report"; + + public static final String REPORT_TYPE_WEEKLY = "weekly"; + public static final String REPORT_TYPE_MONTHLY = "monthly"; + public static final String REPORT_TYPE_PROJECT = "project"; + + public static final String BIZ_TYPE_WEEKLY = "weekly_report"; + public static final String BIZ_TYPE_MONTHLY = "monthly_report"; + public static final String BIZ_TYPE_PROJECT = "project_report"; + + public static final String STATUS_DRAFT = "draft"; + public static final String STATUS_PENDING_APPROVAL = "pending_approval"; + public static final String STATUS_APPROVED = "approved"; + public static final String STATUS_REJECTED = "rejected"; + + public static final String ACTION_CREATE = "create"; + public static final String ACTION_UPDATE = "update"; + public static final String ACTION_DELETE = "delete"; + public static final String ACTION_SUBMIT = "submit"; + public static final String ACTION_RESUBMIT = "resubmit"; + public static final String ACTION_APPROVE = "approve"; + public static final String ACTION_REJECT = "reject"; + + public static final String PERMISSION_QUERY = "project:work-report:query"; + public static final String PERMISSION_CREATE = "project:work-report:create"; + public static final String PERMISSION_UPDATE = "project:work-report:update"; + public static final String PERMISSION_DELETE = "project:work-report:delete"; + public static final String PERMISSION_APPROVE = "project:work-report:approve"; + public static final String PERMISSION_EXPORT = "project:work-report:export"; + public static final String PERMISSION_PROJECT_OWNER = "project:work-report:project-owner"; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/OvertimeApplicationController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/OvertimeApplicationController.java index a2557b5..19dc43a 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/OvertimeApplicationController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/OvertimeApplicationController.java @@ -5,6 +5,7 @@ import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.excel.core.util.ExcelUtils; import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants; +import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationApprovalRecordRespVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO; @@ -53,7 +54,7 @@ public class OvertimeApplicationController { } @PutMapping("/{id}") - @Operation(summary = "退回或撤销后修改并重新提交加班申请") + @Operation(summary = "退回后修改并重新提交加班申请") @PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')") public CommonResult resubmit(@PathVariable("id") Long id, @Valid @RequestBody OvertimeApplicationSaveReqVO reqVO) { @@ -107,17 +108,8 @@ public class OvertimeApplicationController { return success(true); } - @PostMapping("/{id}/cancel") - @Operation(summary = "撤销加班申请") - @PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')") - public CommonResult cancel(@PathVariable("id") Long id, - @Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) { - overtimeApplicationService.cancel(id, reqVO); - return success(true); - } - @DeleteMapping("/{id}") - @Operation(summary = "删除已撤销的加班申请") + @Operation(summary = "删除已退回的加班申请") @PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_DELETE + "')") public CommonResult delete(@PathVariable("id") Long id) { overtimeApplicationService.deleteApplication(id); @@ -131,6 +123,13 @@ public class OvertimeApplicationController { return success(overtimeApplicationService.getStatusLogs(id)); } + @GetMapping("/{id}/approval-records") + @Operation(summary = "获取加班申请审核记录") + @PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')") + public CommonResult> approvalRecords(@PathVariable("id") Long id) { + return success(overtimeApplicationService.getApprovalRecords(id)); + } + @GetMapping("/export") @Operation(summary = "导出我的加班申请") @PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_EXPORT + "')") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationApprovalRecordRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationApprovalRecordRespVO.java new file mode 100644 index 0000000..7b8e20e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationApprovalRecordRespVO.java @@ -0,0 +1,29 @@ +package com.njcn.rdms.module.project.controller.admin.overtime.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 加班申请审核记录 Response VO") +@Data +public class OvertimeApplicationApprovalRecordRespVO { + + private Long id; + + private Long overtimeApplicationId; + + private Long statusLogId; + + private Integer approvalRound; + + private String conclusion; + + private String opinion; + + private Long auditorUserId; + + private String auditorName; + + private LocalDateTime createTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationStatusActionReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationStatusActionReqVO.java index f81b2e1..9d1a706 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationStatusActionReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationStatusActionReqVO.java @@ -8,7 +8,7 @@ import lombok.Data; @Data public class OvertimeApplicationStatusActionReqVO { - @Schema(description = "动作原因或审核意见。是否必填以状态机配置为准;当前退回必填,撤销选填", + @Schema(description = "动作原因或审核意见。是否必填以状态机配置为准;当前退回必填", example = "请补充加班内容") @Size(max = 1000, message = "动作原因长度不能超过 1000 个字符") private String reason; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/WorkReportExportResponseUtils.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/WorkReportExportResponseUtils.java new file mode 100644 index 0000000..bc90347 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/WorkReportExportResponseUtils.java @@ -0,0 +1,20 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common; + +import com.njcn.rdms.framework.common.util.http.HttpUtils; +import com.njcn.rdms.module.project.service.workreport.export.WorkReportExportFile; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public final class WorkReportExportResponseUtils { + + private WorkReportExportResponseUtils() { + } + + public static void write(HttpServletResponse response, WorkReportExportFile file) throws IOException { + response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(file.filename())); + response.setContentType(file.contentType()); + response.setContentLength(file.content().length); + response.getOutputStream().write(file.content()); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/WorkReportStatusController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/WorkReportStatusController.java new file mode 100644 index 0000000..287795b --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/WorkReportStatusController.java @@ -0,0 +1,35 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusDictRespVO; +import com.njcn.rdms.module.project.service.workreport.common.WorkReportStatusService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 工作报告状态") +@RestController +@RequestMapping("/project/work-reports") +@Validated +public class WorkReportStatusController { + + @Resource + private WorkReportStatusService workReportStatusService; + + @GetMapping("/status/dict") + @Operation(summary = "获取工作报告所有状态字典") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getStatusDict() { + return success(workReportStatusService.getStatusDict()); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportPlanItemReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportPlanItemReqVO.java new file mode 100644 index 0000000..19c07ff --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportPlanItemReqVO.java @@ -0,0 +1,19 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 个人报告计划项 Request VO") +@Data +public class PersonalReportPlanItemReqVO { + + private Integer itemNumber; + + private String itemTitle; + + private String targetText; + + private Object targetJson; + + private String supportNeed; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportPlanItemRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportPlanItemRespVO.java new file mode 100644 index 0000000..5916d65 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportPlanItemRespVO.java @@ -0,0 +1,21 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 个人报告计划项 Response VO") +@Data +public class PersonalReportPlanItemRespVO { + + private Long id; + + private Integer itemNumber; + + private String itemTitle; + + private String targetText; + + private Object targetJson; + + private String supportNeed; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportReviewItemReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportReviewItemReqVO.java new file mode 100644 index 0000000..ed395d1 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportReviewItemReqVO.java @@ -0,0 +1,23 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - 个人报告回顾项 Request VO") +@Data +public class PersonalReportReviewItemReqVO { + + private Integer itemNumber; + + private String itemTitle; + + private BigDecimal workHours; + + private String contentText; + + private Object contentJson; + + private String reflectionText; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportReviewItemRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportReviewItemRespVO.java new file mode 100644 index 0000000..ec8bb85 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/PersonalReportReviewItemRespVO.java @@ -0,0 +1,25 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - 个人报告回顾项 Response VO") +@Data +public class PersonalReportReviewItemRespVO { + + private Long id; + + private Integer itemNumber; + + private String itemTitle; + + private BigDecimal workHours; + + private String contentText; + + private Object contentJson; + + private String reflectionText; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportApprovalRecordRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportApprovalRecordRespVO.java new file mode 100644 index 0000000..59fb02d --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportApprovalRecordRespVO.java @@ -0,0 +1,27 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 工作报告审核记录 Response VO") +@Data +public class WorkReportApprovalRecordRespVO { + + private Long id; + + private Long statusLogId; + + private Integer approvalRound; + + private String conclusion; + + private String opinion; + + private Long auditorUserId; + + private String auditorName; + + private LocalDateTime createTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportBasePageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportBasePageReqVO.java new file mode 100644 index 0000000..0c1aba8 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportBasePageReqVO.java @@ -0,0 +1,32 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import com.njcn.rdms.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Data +@EqualsAndHashCode(callSuper = true) +public class WorkReportBasePageReqVO extends PageParam { + + @Schema(description = "关键字") + private String keyword; + + @Schema(description = "状态编码") + private String statusCode; + + @Schema(description = "周期开始日期范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate[] periodStartDate; + + @Schema(description = "提交时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] submitTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportMemberSnapshotRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportMemberSnapshotRespVO.java new file mode 100644 index 0000000..f734cf8 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportMemberSnapshotRespVO.java @@ -0,0 +1,13 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 项目成员快照 Response VO") +@Data +public class WorkReportMemberSnapshotRespVO { + + private Long userId; + + private String userName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusActionReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusActionReqVO.java new file mode 100644 index 0000000..3160326 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusActionReqVO.java @@ -0,0 +1,14 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - 工作报告状态动作 Request VO") +@Data +public class WorkReportStatusActionReqVO { + + @Schema(description = "原因或审核意见", example = "请补充下周计划") + @Size(max = 1000, message = "原因或审核意见长度不能超过 1000 个字符") + private String reason; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusDictRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusDictRespVO.java new file mode 100644 index 0000000..72ea1ba --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusDictRespVO.java @@ -0,0 +1,27 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 工作报告状态字典 Response VO") +@Data +public class WorkReportStatusDictRespVO { + + @Schema(description = "状态编码", example = "draft") + private String statusCode; + + @Schema(description = "状态名称", example = "待提交") + private String statusName; + + @Schema(description = "排序", example = "10") + private Integer sort; + + @Schema(description = "是否初始态") + private Boolean initialFlag; + + @Schema(description = "是否终态") + private Boolean terminalFlag; + + @Schema(description = "是否允许编辑") + private Boolean allowEdit; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusLogRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusLogRespVO.java new file mode 100644 index 0000000..bab9053 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/common/vo/WorkReportStatusLogRespVO.java @@ -0,0 +1,35 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 工作报告状态日志 Response VO") +@Data +public class WorkReportStatusLogRespVO { + + private Long id; + + private String reportType; + + private Long reportId; + + private String actionType; + + private String fromStatus; + + private String toStatus; + + private String reason; + + private Long operatorUserId; + + private String operatorName; + + private String periodLabelSnapshot; + + private String remark; + + private LocalDateTime createTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/MonthlyReportController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/MonthlyReportController.java new file mode 100644 index 0000000..4acf74a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/MonthlyReportController.java @@ -0,0 +1,172 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly; + +import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog; +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.excel.core.util.ExcelUtils; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.common.WorkReportExportResponseUtils; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportApproveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportContentExportReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportExportVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO; +import com.njcn.rdms.module.project.service.workreport.export.WorkReportContentExportService; +import com.njcn.rdms.module.project.service.workreport.monthly.MonthlyReportService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.List; + +import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 个人月报") +@RestController +@RequestMapping("/project/work-reports/monthly") +@Validated +public class MonthlyReportController { + + @Resource + private MonthlyReportService monthlyReportService; + @Resource + private WorkReportContentExportService workReportContentExportService; + + @GetMapping("/init") + @Operation(summary = "获取月报新建默认数据") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") + public CommonResult initMonthlyReport() { + return success(monthlyReportService.initMonthlyReport()); + } + + @GetMapping("/default-draft") + @Operation(summary = "预览月报默认稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") + public CommonResult previewMonthlyDefaultDraft(@Valid MonthlyReportDefaultDraftReqVO reqVO) { + return success(monthlyReportService.previewMonthlyDefaultDraft(reqVO)); + } + + @PostMapping + @Operation(summary = "新建月报草稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") + public CommonResult createMonthlyReport(@Valid @RequestBody MonthlyReportSaveReqVO reqVO) { + return success(monthlyReportService.createMonthlyReport(reqVO)); + } + + @PutMapping("/{id}") + @Operation(summary = "修改月报草稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_UPDATE + "')") + public CommonResult updateMonthlyReport(@PathVariable("id") Long id, + @Valid @RequestBody MonthlyReportSaveReqVO reqVO) { + monthlyReportService.updateMonthlyReport(id, reqVO); + return success(true); + } + + @GetMapping("/{id}") + @Operation(summary = "获取月报详情") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult getMonthlyReport(@PathVariable("id") Long id) { + return success(monthlyReportService.getMonthlyReport(id)); + } + + @GetMapping("/page") + @Operation(summary = "获取我的月报分页") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getMonthlyReportPage(@Valid MonthlyReportPageReqVO reqVO) { + return success(monthlyReportService.getMonthlyReportPage(reqVO)); + } + + @GetMapping("/approval-page") + @Operation(summary = "获取待我审批的月报分页") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_APPROVE + "')") + public CommonResult> getMonthlyApprovalPage(@Valid MonthlyReportPageReqVO reqVO) { + return success(monthlyReportService.getMonthlyApprovalPage(reqVO)); + } + + @PostMapping("/{id}/submit") + @Operation(summary = "提交月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_UPDATE + "')") + public CommonResult submitMonthlyReport(@PathVariable("id") Long id) { + monthlyReportService.submitMonthlyReport(id); + return success(true); + } + + @PostMapping("/{id}/approve") + @Operation(summary = "审批通过月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_APPROVE + "')") + public CommonResult approveMonthlyReport(@PathVariable("id") Long id, + @Valid @RequestBody MonthlyReportApproveReqVO reqVO) { + monthlyReportService.approveMonthlyReport(id, reqVO); + return success(true); + } + + @PostMapping("/{id}/reject") + @Operation(summary = "退回月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_APPROVE + "')") + public CommonResult rejectMonthlyReport(@PathVariable("id") Long id, + @Valid @RequestBody WorkReportStatusActionReqVO reqVO) { + monthlyReportService.rejectMonthlyReport(id, reqVO); + return success(true); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_DELETE + "')") + public CommonResult deleteMonthlyReport(@PathVariable("id") Long id) { + monthlyReportService.deleteMonthlyReport(id); + return success(true); + } + + @GetMapping("/{id}/status-logs") + @Operation(summary = "获取月报状态日志") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getMonthlyStatusLogs(@PathVariable("id") Long id) { + return success(monthlyReportService.getMonthlyStatusLogs(id)); + } + + @GetMapping("/{id}/approval-records") + @Operation(summary = "获取月报审批记录") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getMonthlyApprovalRecords(@PathVariable("id") Long id) { + return success(monthlyReportService.getMonthlyApprovalRecords(id)); + } + + @GetMapping("/export") + @Operation(summary = "导出我的月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_EXPORT + "')") + @ApiAccessLog(operateType = EXPORT) + public void exportMonthlyReport(@Valid MonthlyReportPageReqVO reqVO, HttpServletResponse response) + throws IOException { + ExcelUtils.write(response, "个人月报_" + LocalDate.now() + ".xls", "个人月报", + MonthlyReportExportVO.class, monthlyReportService.getMonthlyExportList(reqVO)); + } + + @PostMapping("/content-export") + @Operation(summary = "导出月报内容 Word") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_EXPORT + "')") + @ApiAccessLog(operateType = EXPORT) + public void exportMonthlyReportContent(@Valid @RequestBody MonthlyReportContentExportReqVO reqVO, + HttpServletResponse response) throws IOException { + WorkReportExportResponseUtils.write(response, workReportContentExportService.exportMonthly(reqVO)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApprovalRecordRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApprovalRecordRespVO.java new file mode 100644 index 0000000..2bc7682 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApprovalRecordRespVO.java @@ -0,0 +1,50 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 月报审核记录 Response VO") +@Data +public class MonthlyReportApprovalRecordRespVO { + + private Long id; + + private Long statusLogId; + + private Integer approvalRound; + + private String conclusion; + + private String opinion; + + private LocalDate meetingDate; + + private String strengthDesc; + + private String strengthExample; + + private String weaknessDesc; + + private String weaknessExample; + + private String improvementSuggestion; + + private String performanceResult; + + private String employeeSignName; + + private LocalDate employeeSignedDate; + + private String supervisorSignName; + + private LocalDate supervisorSignedDate; + + private Long auditorUserId; + + private String auditorName; + + private LocalDateTime createTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApproveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApproveReqVO.java new file mode 100644 index 0000000..eb2791d --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApproveReqVO.java @@ -0,0 +1,57 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 月报审核通过 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class MonthlyReportApproveReqVO extends WorkReportStatusActionReqVO { + + @Schema(description = "面谈时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + @NotNull + private LocalDate meetingDate; + + @Schema(description = "优势描述") + private String strengthDesc; + + @Schema(description = "优势行为事例") + private String strengthExample; + + @Schema(description = "劣势描述") + private String weaknessDesc; + + @Schema(description = "劣势行为事例") + private String weaknessExample; + + @Schema(description = "改进建议") + private String improvementSuggestion; + + @Schema(description = "绩效考核结果") + @NotBlank(message = "绩效考核结果不能为空") + private String performanceResult; + + @Schema(description = "被考核人签名") + private String employeeSignName; + + @Schema(description = "被考核人签字日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate employeeSignedDate; + + @Schema(description = "上级签名") + private String supervisorSignName; + + @Schema(description = "上级签字日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate supervisorSignedDate; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportContentExportReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportContentExportReqVO.java new file mode 100644 index 0000000..d90c08e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportContentExportReqVO.java @@ -0,0 +1,19 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - 月报内容导出 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class MonthlyReportContentExportReqVO extends MonthlyReportPageReqVO { + + @Schema(description = "是否按当前搜索条件全量导出") + private Boolean exportAll; + + @Schema(description = "选中的月报编号列表;exportAll=false 时必填") + private List ids; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportDefaultDraftReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportDefaultDraftReqVO.java new file mode 100644 index 0000000..2ceb835 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportDefaultDraftReqVO.java @@ -0,0 +1,30 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 月报默认稿预览 Request VO") +@Data +public class MonthlyReportDefaultDraftReqVO { + + @NotBlank(message = "周期编码不能为空") + private String periodKey; + + @NotBlank(message = "周期名称不能为空") + private String periodLabel; + + @NotNull(message = "周期开始日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodStartDate; + + @NotNull(message = "周期结束日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodEndDate; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportExportVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportExportVO.java new file mode 100644 index 0000000..82818a9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportExportVO.java @@ -0,0 +1,44 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@ExcelIgnoreUnannotated +public class MonthlyReportExportVO { + + @ExcelProperty("填报人") + private String reporterName; + + @ExcelProperty("直属上级") + private String supervisorName; + + @ExcelProperty("周期") + private String periodLabel; + + @ExcelProperty("开始日期") + private LocalDate periodStartDate; + + @ExcelProperty("结束日期") + private LocalDate periodEndDate; + + @ExcelProperty("工时") + private BigDecimal totalWorkHours; + + @ExcelProperty("状态") + private String statusName; + + @ExcelProperty("审核意见") + private String approvalComment; + + @ExcelProperty("提交时间") + private LocalDateTime submitTime; + + @ExcelProperty("审核时间") + private LocalDateTime approvalTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportPageReqVO.java new file mode 100644 index 0000000..d50e782 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportPageReqVO.java @@ -0,0 +1,12 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportBasePageReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 月报分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class MonthlyReportPageReqVO extends WorkReportBasePageReqVO { +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRespVO.java new file mode 100644 index 0000000..a2afa01 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRespVO.java @@ -0,0 +1,41 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 月报 Response VO") +@Data +public class MonthlyReportRespVO { + + private Long id; + private Long reporterId; + private String reporterName; + private String reporterDeptName; + private String reporterPostName; + private Long supervisorUserId; + private String supervisorName; + private String periodKey; + private String periodLabel; + private LocalDate periodStartDate; + private LocalDate periodEndDate; + private String statusCode; + private String statusName; + private Boolean allowEdit; + private Boolean terminal; + private BigDecimal totalWorkHours; + private String approvalComment; + private String lastStatusReason; + private LocalDateTime submitTime; + private LocalDateTime approvalTime; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private List reviewItems; + private List planItems; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSaveReqVO.java new file mode 100644 index 0000000..fb925fd --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSaveReqVO.java @@ -0,0 +1,40 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.util.List; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 月报保存 Request VO") +@Data +public class MonthlyReportSaveReqVO { + + @NotBlank(message = "周期编码不能为空") + private String periodKey; + + @NotBlank(message = "周期名称不能为空") + private String periodLabel; + + @NotNull(message = "周期开始日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodStartDate; + + @NotNull(message = "周期结束日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodEndDate; + + @Valid + private List reviewItems; + + @Valid + private List planItems; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/ProjectReportController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/ProjectReportController.java new file mode 100644 index 0000000..f4ddc65 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/ProjectReportController.java @@ -0,0 +1,181 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project; + +import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog; +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.excel.core.util.ExcelUtils; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.common.WorkReportExportResponseUtils; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportContentExportReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportExportVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportOwnerProjectOptionRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportSaveReqVO; +import com.njcn.rdms.module.project.service.workreport.export.WorkReportContentExportService; +import com.njcn.rdms.module.project.service.workreport.project.ProjectReportService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.List; + +import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 项目半月报") +@RestController +@RequestMapping("/project/work-reports/project") +@Validated +public class ProjectReportController { + + @Resource + private ProjectReportService projectReportService; + @Resource + private WorkReportContentExportService workReportContentExportService; + + @GetMapping("/owner-project-options") + @Operation(summary = "获取项目半月报负责人可选项目列表") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_PROJECT_OWNER + "')") + public CommonResult> getOwnerProjectOptions() { + return success(projectReportService.getOwnerProjectOptions()); + } + + @GetMapping("/init") + @Operation(summary = "获取项目半月报新建默认数据") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_PROJECT_OWNER + "')") + public CommonResult initProjectReport(@RequestParam("projectId") Long projectId) { + return success(projectReportService.initProjectReport(projectId)); + } + + @GetMapping("/{projectId}/default-draft") + @Operation(summary = "预览项目半月报默认稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_PROJECT_OWNER + "')") + public CommonResult previewProjectDefaultDraft(@PathVariable("projectId") Long projectId, + @Valid ProjectReportDefaultDraftReqVO reqVO) { + return success(projectReportService.previewProjectDefaultDraft(projectId, reqVO)); + } + + @PostMapping + @Operation(summary = "新建项目半月报草稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_PROJECT_OWNER + "')") + public CommonResult createProjectReport(@Valid @RequestBody ProjectReportSaveReqVO reqVO) { + return success(projectReportService.createProjectReport(reqVO)); + } + + @PutMapping("/{id}") + @Operation(summary = "修改项目半月报草稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_PROJECT_OWNER + "')") + public CommonResult updateProjectReport(@PathVariable("id") Long id, + @Valid @RequestBody ProjectReportSaveReqVO reqVO) { + projectReportService.updateProjectReport(id, reqVO); + return success(true); + } + + @GetMapping("/{id}") + @Operation(summary = "获取项目半月报详情") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult getProjectReport(@PathVariable("id") Long id) { + return success(projectReportService.getProjectReport(id)); + } + + @GetMapping("/page") + @Operation(summary = "获取我的项目半月报分页") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getProjectReportPage(@Valid ProjectReportPageReqVO reqVO) { + return success(projectReportService.getProjectReportPage(reqVO)); + } + + @GetMapping("/approval-page") + @Operation(summary = "获取待我审批的项目半月报分页") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_APPROVE + "')") + public CommonResult> getProjectApprovalPage(@Valid ProjectReportPageReqVO reqVO) { + return success(projectReportService.getProjectApprovalPage(reqVO)); + } + + @PostMapping("/{id}/submit") + @Operation(summary = "提交项目半月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_UPDATE + "')") + public CommonResult submitProjectReport(@PathVariable("id") Long id) { + projectReportService.submitProjectReport(id); + return success(true); + } + + @PostMapping("/{id}/approve") + @Operation(summary = "审批通过项目半月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_APPROVE + "')") + public CommonResult approveProjectReport(@PathVariable("id") Long id, + @Valid @RequestBody WorkReportStatusActionReqVO reqVO) { + projectReportService.approveProjectReport(id, reqVO); + return success(true); + } + + @PostMapping("/{id}/reject") + @Operation(summary = "退回项目半月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_APPROVE + "')") + public CommonResult rejectProjectReport(@PathVariable("id") Long id, + @Valid @RequestBody WorkReportStatusActionReqVO reqVO) { + projectReportService.rejectProjectReport(id, reqVO); + return success(true); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除项目半月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_DELETE + "')") + public CommonResult deleteProjectReport(@PathVariable("id") Long id) { + projectReportService.deleteProjectReport(id); + return success(true); + } + + @GetMapping("/{id}/status-logs") + @Operation(summary = "获取项目半月报状态日志") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getProjectStatusLogs(@PathVariable("id") Long id) { + return success(projectReportService.getProjectStatusLogs(id)); + } + + @GetMapping("/{id}/approval-records") + @Operation(summary = "获取项目半月报审批记录") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getProjectApprovalRecords(@PathVariable("id") Long id) { + return success(projectReportService.getProjectApprovalRecords(id)); + } + + @GetMapping("/export") + @Operation(summary = "导出我的项目半月报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_EXPORT + "')") + @ApiAccessLog(operateType = EXPORT) + public void exportProjectReport(@Valid ProjectReportPageReqVO reqVO, HttpServletResponse response) + throws IOException { + ExcelUtils.write(response, "项目半月报_" + LocalDate.now() + ".xls", "项目半月报", + ProjectReportExportVO.class, projectReportService.getProjectExportList(reqVO)); + } + + @PostMapping("/content-export") + @Operation(summary = "导出项目半月报内容 Word") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_EXPORT + "')") + @ApiAccessLog(operateType = EXPORT) + public void exportProjectReportContent(@Valid @RequestBody ProjectReportContentExportReqVO reqVO, + HttpServletResponse response) throws IOException { + WorkReportExportResponseUtils.write(response, workReportContentExportService.exportProject(reqVO)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportContentExportReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportContentExportReqVO.java new file mode 100644 index 0000000..29551ca --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportContentExportReqVO.java @@ -0,0 +1,19 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - 项目半月报内容导出 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectReportContentExportReqVO extends ProjectReportPageReqVO { + + @Schema(description = "是否按当前搜索条件全量导出") + private Boolean exportAll; + + @Schema(description = "选中的项目半月报编号列表;exportAll=false 时必填") + private List ids; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportDefaultDraftReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportDefaultDraftReqVO.java new file mode 100644 index 0000000..4bcf25c --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportDefaultDraftReqVO.java @@ -0,0 +1,33 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 项目半月报默认稿预览 Request VO") +@Data +public class ProjectReportDefaultDraftReqVO { + + @NotBlank(message = "周期编码不能为空") + private String periodKey; + + @NotBlank(message = "周期名称不能为空") + private String periodLabel; + + @NotNull(message = "周期开始日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodStartDate; + + @NotNull(message = "周期结束日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodEndDate; + + @NotNull(message = "上半月/下半月标记不能为空") + private Integer flag; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportExportVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportExportVO.java new file mode 100644 index 0000000..8fe43d6 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportExportVO.java @@ -0,0 +1,47 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@ExcelIgnoreUnannotated +public class ProjectReportExportVO { + + @ExcelProperty("项目名称") + private String projectName; + + @ExcelProperty("填报人") + private String projectOwnerName; + + @ExcelProperty("直属上级") + private String supervisorName; + + @ExcelProperty("周期") + private String periodLabel; + + @ExcelProperty("开始日期") + private LocalDate periodStartDate; + + @ExcelProperty("结束日期") + private LocalDate periodEndDate; + + @ExcelProperty("工时") + private BigDecimal totalWorkHours; + + @ExcelProperty("状态") + private String statusName; + + @ExcelProperty("审核意见") + private String approvalComment; + + @ExcelProperty("提交时间") + private LocalDateTime submitTime; + + @ExcelProperty("审核时间") + private LocalDateTime approvalTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportItemReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportItemReqVO.java new file mode 100644 index 0000000..69155d0 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportItemReqVO.java @@ -0,0 +1,19 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - 项目半月报工作项 Request VO") +@Data +public class ProjectReportItemReqVO { + + private String itemTitle; + + private BigDecimal workHours; + + private String priorityCode; + + private BigDecimal progressRate; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportItemRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportItemRespVO.java new file mode 100644 index 0000000..8beb64d --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportItemRespVO.java @@ -0,0 +1,21 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - 项目半月报工作项 Response VO") +@Data +public class ProjectReportItemRespVO { + + private Long id; + + private String itemTitle; + + private BigDecimal workHours; + + private String priorityCode; + + private BigDecimal progressRate; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportOwnerProjectOptionRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportOwnerProjectOptionRespVO.java new file mode 100644 index 0000000..70fdf3e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportOwnerProjectOptionRespVO.java @@ -0,0 +1,18 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 项目半月报负责人可选项目 Response VO") +@Data +public class ProjectReportOwnerProjectOptionRespVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "项目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CNPJ2026001") + private String projectCode; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户交付项目") + private String projectName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportPageReqVO.java new file mode 100644 index 0000000..134fdfc --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportPageReqVO.java @@ -0,0 +1,18 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportBasePageReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 项目半月报分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectReportPageReqVO extends WorkReportBasePageReqVO { + + @Schema(description = "项目编号") + private Long projectId; + + @Schema(description = "上半月/下半月标记") + private Integer flag; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportRespVO.java new file mode 100644 index 0000000..d99882e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportRespVO.java @@ -0,0 +1,47 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportMemberSnapshotRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 项目半月报 Response VO") +@Data +public class ProjectReportRespVO { + + private Long id; + private Long projectId; + private String projectName; + private Long projectOwnerId; + private String projectOwnerName; + private String technicalOwnerName; + private List projectMemberSnapshot; + private Long supervisorUserId; + private String supervisorName; + private String periodKey; + private String periodLabel; + private LocalDate periodStartDate; + private LocalDate periodEndDate; + private Integer flag; + private String statusCode; + private String statusName; + private Boolean allowEdit; + private Boolean terminal; + private String projectStatusDesc; + private String projectProgressPlan; + private String projectKeyPoints; + private String projectProblems; + private BigDecimal totalWorkHours; + private String approvalComment; + private String lastStatusReason; + private LocalDateTime submitTime; + private LocalDateTime approvalTime; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private List currentItems; + private List nextItems; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportSaveReqVO.java new file mode 100644 index 0000000..55608f3 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportSaveReqVO.java @@ -0,0 +1,52 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.util.List; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 项目半月报保存 Request VO") +@Data +public class ProjectReportSaveReqVO { + + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @NotBlank(message = "周期编码不能为空") + private String periodKey; + + @NotBlank(message = "周期名称不能为空") + private String periodLabel; + + @NotNull(message = "周期开始日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodStartDate; + + @NotNull(message = "周期结束日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodEndDate; + + @NotNull(message = "上半月/下半月标记不能为空") + private Integer flag; + + private String projectStatusDesc; + + private String projectProgressPlan; + + private String projectKeyPoints; + + private String projectProblems; + + @Valid + private List currentItems; + + @Valid + private List nextItems; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/WeeklyReportController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/WeeklyReportController.java new file mode 100644 index 0000000..78d4442 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/WeeklyReportController.java @@ -0,0 +1,171 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly; + +import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog; +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.excel.core.util.ExcelUtils; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.common.WorkReportExportResponseUtils; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportContentExportReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportExportVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportSaveReqVO; +import com.njcn.rdms.module.project.service.workreport.export.WorkReportContentExportService; +import com.njcn.rdms.module.project.service.workreport.weekly.WeeklyReportService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.List; + +import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 个人周报") +@RestController +@RequestMapping("/project/work-reports/weekly") +@Validated +public class WeeklyReportController { + + @Resource + private WeeklyReportService weeklyReportService; + @Resource + private WorkReportContentExportService workReportContentExportService; + + @GetMapping("/init") + @Operation(summary = "获取周报新建默认数据") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") + public CommonResult initWeeklyReport() { + return success(weeklyReportService.initWeeklyReport()); + } + + @GetMapping("/default-draft") + @Operation(summary = "预览周报默认稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") + public CommonResult previewWeeklyDefaultDraft(@Valid WeeklyReportDefaultDraftReqVO reqVO) { + return success(weeklyReportService.previewWeeklyDefaultDraft(reqVO)); + } + + @PostMapping + @Operation(summary = "新建周报草稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") + public CommonResult createWeeklyReport(@Valid @RequestBody WeeklyReportSaveReqVO reqVO) { + return success(weeklyReportService.createWeeklyReport(reqVO)); + } + + @PutMapping("/{id}") + @Operation(summary = "修改周报草稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_UPDATE + "')") + public CommonResult updateWeeklyReport(@PathVariable("id") Long id, + @Valid @RequestBody WeeklyReportSaveReqVO reqVO) { + weeklyReportService.updateWeeklyReport(id, reqVO); + return success(true); + } + + @GetMapping("/{id}") + @Operation(summary = "获取周报详情") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult getWeeklyReport(@PathVariable("id") Long id) { + return success(weeklyReportService.getWeeklyReport(id)); + } + + @GetMapping("/page") + @Operation(summary = "获取我的周报分页") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getWeeklyReportPage(@Valid WeeklyReportPageReqVO reqVO) { + return success(weeklyReportService.getWeeklyReportPage(reqVO)); + } + + @GetMapping("/approval-page") + @Operation(summary = "获取待我审批的周报分页") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_APPROVE + "')") + public CommonResult> getWeeklyApprovalPage(@Valid WeeklyReportPageReqVO reqVO) { + return success(weeklyReportService.getWeeklyApprovalPage(reqVO)); + } + + @PostMapping("/{id}/submit") + @Operation(summary = "提交周报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_UPDATE + "')") + public CommonResult submitWeeklyReport(@PathVariable("id") Long id) { + weeklyReportService.submitWeeklyReport(id); + return success(true); + } + + @PostMapping("/{id}/approve") + @Operation(summary = "审批通过周报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_APPROVE + "')") + public CommonResult approveWeeklyReport(@PathVariable("id") Long id, + @Valid @RequestBody WorkReportStatusActionReqVO reqVO) { + weeklyReportService.approveWeeklyReport(id, reqVO); + return success(true); + } + + @PostMapping("/{id}/reject") + @Operation(summary = "退回周报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_APPROVE + "')") + public CommonResult rejectWeeklyReport(@PathVariable("id") Long id, + @Valid @RequestBody WorkReportStatusActionReqVO reqVO) { + weeklyReportService.rejectWeeklyReport(id, reqVO); + return success(true); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除周报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_DELETE + "')") + public CommonResult deleteWeeklyReport(@PathVariable("id") Long id) { + weeklyReportService.deleteWeeklyReport(id); + return success(true); + } + + @GetMapping("/{id}/status-logs") + @Operation(summary = "获取周报状态日志") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getWeeklyStatusLogs(@PathVariable("id") Long id) { + return success(weeklyReportService.getWeeklyStatusLogs(id)); + } + + @GetMapping("/{id}/approval-records") + @Operation(summary = "获取周报审批记录") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_QUERY + "')") + public CommonResult> getWeeklyApprovalRecords(@PathVariable("id") Long id) { + return success(weeklyReportService.getWeeklyApprovalRecords(id)); + } + + @GetMapping("/export") + @Operation(summary = "导出我的周报") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_EXPORT + "')") + @ApiAccessLog(operateType = EXPORT) + public void exportWeeklyReport(@Valid WeeklyReportPageReqVO reqVO, HttpServletResponse response) + throws IOException { + ExcelUtils.write(response, "个人周报_" + LocalDate.now() + ".xls", "个人周报", + WeeklyReportExportVO.class, weeklyReportService.getWeeklyExportList(reqVO)); + } + + @PostMapping("/content-export") + @Operation(summary = "导出周报内容 Word") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_EXPORT + "')") + @ApiAccessLog(operateType = EXPORT) + public void exportWeeklyReportContent(@Valid @RequestBody WeeklyReportContentExportReqVO reqVO, + HttpServletResponse response) throws IOException { + WorkReportExportResponseUtils.write(response, workReportContentExportService.exportWeekly(reqVO)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportContentExportReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportContentExportReqVO.java new file mode 100644 index 0000000..06a736f --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportContentExportReqVO.java @@ -0,0 +1,19 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - 周报内容导出 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class WeeklyReportContentExportReqVO extends WeeklyReportPageReqVO { + + @Schema(description = "是否按当前搜索条件全量导出") + private Boolean exportAll; + + @Schema(description = "选中的周报编号列表;exportAll=false 时必填") + private List ids; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportDefaultDraftReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportDefaultDraftReqVO.java new file mode 100644 index 0000000..d2f2d43 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportDefaultDraftReqVO.java @@ -0,0 +1,30 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 周报默认稿预览 Request VO") +@Data +public class WeeklyReportDefaultDraftReqVO { + + @NotBlank(message = "周期编码不能为空") + private String periodKey; + + @NotBlank(message = "周期名称不能为空") + private String periodLabel; + + @NotNull(message = "周期开始日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodStartDate; + + @NotNull(message = "周期结束日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodEndDate; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportExportVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportExportVO.java new file mode 100644 index 0000000..4a5bb06 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportExportVO.java @@ -0,0 +1,50 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@ExcelIgnoreUnannotated +public class WeeklyReportExportVO { + + @ExcelProperty("填报人") + private String reporterName; + + @ExcelProperty("直属上级") + private String supervisorName; + + @ExcelProperty("周期") + private String periodLabel; + + @ExcelProperty("开始日期") + private LocalDate periodStartDate; + + @ExcelProperty("结束日期") + private LocalDate periodEndDate; + + @ExcelProperty("是否出差") + private Boolean isBusinessTrip; + + @ExcelProperty("出差天数") + private BigDecimal totalTravelDays; + + @ExcelProperty("工时") + private BigDecimal totalWorkHours; + + @ExcelProperty("状态") + private String statusName; + + @ExcelProperty("审核意见") + private String approvalComment; + + @ExcelProperty("提交时间") + private LocalDateTime submitTime; + + @ExcelProperty("审核时间") + private LocalDateTime approvalTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportPageReqVO.java new file mode 100644 index 0000000..24ba9aa --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportPageReqVO.java @@ -0,0 +1,15 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportBasePageReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 周报分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class WeeklyReportPageReqVO extends WorkReportBasePageReqVO { + + @Schema(description = "是否出差") + private Boolean isBusinessTrip; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportRespVO.java new file mode 100644 index 0000000..f734cd7 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportRespVO.java @@ -0,0 +1,44 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 周报 Response VO") +@Data +public class WeeklyReportRespVO { + + private Long id; + private Long reporterId; + private String reporterName; + private String reporterDeptName; + private String reporterPostName; + private Long supervisorUserId; + private String supervisorName; + private String periodKey; + private String periodLabel; + private LocalDate periodStartDate; + private LocalDate periodEndDate; + private String statusCode; + private String statusName; + private Boolean allowEdit; + private Boolean terminal; + private Boolean isBusinessTrip; + private BigDecimal totalTravelDays; + private BigDecimal totalWorkHours; + private String approvalComment; + private String lastStatusReason; + private LocalDateTime submitTime; + private LocalDateTime approvalTime; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private List reviewItems; + private List planItems; + private List travelSegments; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportSaveReqVO.java new file mode 100644 index 0000000..8fe9a89 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportSaveReqVO.java @@ -0,0 +1,46 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.util.List; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 周报保存 Request VO") +@Data +public class WeeklyReportSaveReqVO { + + @NotBlank(message = "周期编码不能为空") + private String periodKey; + + @NotBlank(message = "周期名称不能为空") + private String periodLabel; + + @NotNull(message = "周期开始日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodStartDate; + + @NotNull(message = "周期结束日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodEndDate; + + @NotNull(message = "是否出差不能为空") + private Boolean isBusinessTrip; + + @Valid + private List reviewItems; + + @Valid + private List planItems; + + @Valid + private List travelSegments; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportTravelSegmentReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportTravelSegmentReqVO.java new file mode 100644 index 0000000..de4b9b6 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportTravelSegmentReqVO.java @@ -0,0 +1,27 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 周报出差分段 Request VO") +@Data +public class WeeklyReportTravelSegmentReqVO { + + private Integer sort; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate startDate; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate endDate; + + private BigDecimal travelDays; + + private String location; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportTravelSegmentRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportTravelSegmentRespVO.java new file mode 100644 index 0000000..ce7438a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportTravelSegmentRespVO.java @@ -0,0 +1,24 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Schema(description = "管理后台 - 周报出差分段 Response VO") +@Data +public class WeeklyReportTravelSegmentRespVO { + + private Long id; + + private Integer sort; + + private LocalDate startDate; + + private LocalDate endDate; + + private BigDecimal travelDays; + + private String location; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/overtime/OvertimeApplicationApprovalRecordDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/overtime/OvertimeApplicationApprovalRecordDO.java new file mode 100644 index 0000000..fc391fa --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/overtime/OvertimeApplicationApprovalRecordDO.java @@ -0,0 +1,36 @@ +package com.njcn.rdms.module.project.dal.dataobject.overtime; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 加班申请审核记录。 + */ +@TableName("rdms_overtime_application_approval_record") +@Data +@EqualsAndHashCode(callSuper = true) +public class OvertimeApplicationApprovalRecordDO extends BaseDO { + + @TableId + private Long id; + + private Long overtimeApplicationId; + + private Long statusLogId; + + private Integer approvalRound; + + private String conclusion; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String opinion; + + private Long auditorUserId; + + private String auditorName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/PersonalReportPlanItemDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/PersonalReportPlanItemDO.java new file mode 100644 index 0000000..e598594 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/PersonalReportPlanItemDO.java @@ -0,0 +1,40 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.common; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 个人周报/月报计划项。 + */ +@TableName(value = "rdms_work_report_personal_report_plan_item", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class PersonalReportPlanItemDO extends BaseDO { + + @TableId + private Long id; + + private String reportType; + + private Long reportId; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Integer itemNumber; + + private String itemTitle; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String targetText; + + @TableField(typeHandler = JacksonTypeHandler.class, updateStrategy = FieldStrategy.ALWAYS) + private Object targetJson; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String supportNeed; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/PersonalReportReviewItemDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/PersonalReportReviewItemDO.java new file mode 100644 index 0000000..b0ecbe7 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/PersonalReportReviewItemDO.java @@ -0,0 +1,45 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.common; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 个人周报/月报回顾项。 + */ +@TableName(value = "rdms_work_report_personal_report_review_item", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class PersonalReportReviewItemDO extends BaseDO { + + @TableId + private Long id; + + private String reportType; + + private Long reportId; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Integer itemNumber; + + private String itemTitle; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal workHours; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String contentText; + + @TableField(typeHandler = JacksonTypeHandler.class, updateStrategy = FieldStrategy.ALWAYS) + private Object contentJson; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String reflectionText; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/WorkReportMemberSnapshotItem.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/WorkReportMemberSnapshotItem.java new file mode 100644 index 0000000..b073eb6 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/WorkReportMemberSnapshotItem.java @@ -0,0 +1,14 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.common; + +import lombok.Data; + +/** + * 项目半月报中的项目成员快照。 + */ +@Data +public class WorkReportMemberSnapshotItem { + + private Long userId; + + private String userName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/WorkReportStatusLogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/WorkReportStatusLogDO.java new file mode 100644 index 0000000..65fd9fe --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/common/WorkReportStatusLogDO.java @@ -0,0 +1,44 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.common; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 工作报告状态日志。 + */ +@TableName("rdms_work_report_status_log") +@Data +@EqualsAndHashCode(callSuper = true) +public class WorkReportStatusLogDO extends BaseDO { + + @TableId + private Long id; + + private String reportType; + + private Long reportId; + + private String actionType; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String fromStatus; + + private String toStatus; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String reason; + + private Long operatorUserId; + + private String operatorName; + + private String periodLabelSnapshot; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String remark; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportApprovalRecordDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportApprovalRecordDO.java new file mode 100644 index 0000000..6c953a8 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportApprovalRecordDO.java @@ -0,0 +1,71 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.monthly; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDate; + +/** + * 月报审核记录。 + */ +@TableName("rdms_work_report_monthly_report_approval_record") +@Data +@EqualsAndHashCode(callSuper = true) +public class MonthlyReportApprovalRecordDO extends BaseDO { + + @TableId + private Long id; + + private Long monthlyReportId; + + private Long statusLogId; + + private Integer approvalRound; + + private String conclusion; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String opinion; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDate meetingDate; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String strengthDesc; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String strengthExample; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String weaknessDesc; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String weaknessExample; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String improvementSuggestion; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String performanceResult; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String employeeSignName; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDate employeeSignedDate; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String supervisorSignName; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDate supervisorSignedDate; + + private Long auditorUserId; + + private String auditorName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportDO.java new file mode 100644 index 0000000..6cf08c6 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportDO.java @@ -0,0 +1,64 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.monthly; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 工作报告-月报主表。 + */ +@TableName("rdms_work_report_monthly_report") +@Data +@EqualsAndHashCode(callSuper = true) +public class MonthlyReportDO extends BaseDO { + + @TableId + private Long id; + + private Long reporterId; + + private String reporterName; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String reporterDeptName; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String reporterPostName; + + private Long supervisorUserId; + + private String supervisorName; + + private String periodKey; + + private String periodLabel; + + private LocalDate periodStartDate; + + private LocalDate periodEndDate; + + private String statusCode; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal totalWorkHours; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime submitTime; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime approvalTime; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String approvalComment; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String lastStatusReason; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportApprovalRecordDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportApprovalRecordDO.java new file mode 100644 index 0000000..ee40216 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportApprovalRecordDO.java @@ -0,0 +1,36 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.project; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 项目半月报审核记录。 + */ +@TableName("rdms_work_report_project_report_approval_record") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectReportApprovalRecordDO extends BaseDO { + + @TableId + private Long id; + + private Long projectReportId; + + private Long statusLogId; + + private Integer approvalRound; + + private String conclusion; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String opinion; + + private Long auditorUserId; + + private String auditorName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportCurrentItemDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportCurrentItemDO.java new file mode 100644 index 0000000..d159434 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportCurrentItemDO.java @@ -0,0 +1,36 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.project; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 项目半月报本期工作项。 + */ +@TableName("rdms_work_report_project_report_current_item") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectReportCurrentItemDO extends BaseDO { + + @TableId + private Long id; + + private Long reportId; + + private String itemTitle; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal workHours; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String priorityCode; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal progressRate; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportDO.java new file mode 100644 index 0000000..5a65054 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportDO.java @@ -0,0 +1,85 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.project; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportMemberSnapshotItem; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 工作报告-项目半月报主表。 + */ +@TableName(value = "rdms_work_report_project_report", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectReportDO extends BaseDO { + + @TableId + private Long id; + + private Long projectId; + + private String projectName; + + private Long projectOwnerId; + + private String projectOwnerName; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String technicalOwnerName; + + @TableField(typeHandler = JacksonTypeHandler.class, updateStrategy = FieldStrategy.ALWAYS) + private List projectMemberSnapshot; + + private Long supervisorUserId; + + private String supervisorName; + + private String periodKey; + + private String periodLabel; + + private LocalDate periodStartDate; + + private LocalDate periodEndDate; + + private Integer flag; + + private String statusCode; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String projectStatusDesc; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String projectProgressPlan; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String projectKeyPoints; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String projectProblems; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal totalWorkHours; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime submitTime; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime approvalTime; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String approvalComment; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String lastStatusReason; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportNextItemDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportNextItemDO.java new file mode 100644 index 0000000..56dd6b7 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/project/ProjectReportNextItemDO.java @@ -0,0 +1,33 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.project; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 项目半月报下期工作项。 + */ +@TableName("rdms_work_report_project_report_next_item") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectReportNextItemDO extends BaseDO { + + @TableId + private Long id; + + private Long reportId; + + private String itemTitle; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String priorityCode; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal progressRate; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportApprovalRecordDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportApprovalRecordDO.java new file mode 100644 index 0000000..bf9fe9a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportApprovalRecordDO.java @@ -0,0 +1,36 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.weekly; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 周报审核记录。 + */ +@TableName("rdms_work_report_weekly_report_approval_record") +@Data +@EqualsAndHashCode(callSuper = true) +public class WeeklyReportApprovalRecordDO extends BaseDO { + + @TableId + private Long id; + + private Long weeklyReportId; + + private Long statusLogId; + + private Integer approvalRound; + + private String conclusion; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String opinion; + + private Long auditorUserId; + + private String auditorName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportDO.java new file mode 100644 index 0000000..cc70544 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportDO.java @@ -0,0 +1,69 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.weekly; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 工作报告-周报主表。 + */ +@TableName("rdms_work_report_weekly_report") +@Data +@EqualsAndHashCode(callSuper = true) +public class WeeklyReportDO extends BaseDO { + + @TableId + private Long id; + + private Long reporterId; + + private String reporterName; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String reporterDeptName; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String reporterPostName; + + private Long supervisorUserId; + + private String supervisorName; + + private String periodKey; + + private String periodLabel; + + private LocalDate periodStartDate; + + private LocalDate periodEndDate; + + private String statusCode; + + private Boolean isBusinessTrip; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal totalTravelDays; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal totalWorkHours; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime submitTime; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime approvalTime; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String approvalComment; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String lastStatusReason; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportTravelSegmentDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportTravelSegmentDO.java new file mode 100644 index 0000000..1aa3347 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/weekly/WeeklyReportTravelSegmentDO.java @@ -0,0 +1,36 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.weekly; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 周报出差分段。 + */ +@TableName("rdms_work_report_weekly_report_travel_segment") +@Data +@EqualsAndHashCode(callSuper = true) +public class WeeklyReportTravelSegmentDO extends BaseDO { + + @TableId + private Long id; + + private Long weeklyReportId; + + @TableField("sort") + private Integer sort; + + private LocalDate startDate; + + private LocalDate endDate; + + private BigDecimal travelDays; + + private String location; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/overtime/OvertimeApplicationApprovalRecordMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/overtime/OvertimeApplicationApprovalRecordMapper.java new file mode 100644 index 0000000..83eb091 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/overtime/OvertimeApplicationApprovalRecordMapper.java @@ -0,0 +1,24 @@ +package com.njcn.rdms.module.project.dal.mysql.overtime; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationApprovalRecordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface OvertimeApplicationApprovalRecordMapper extends BaseMapperX { + + default List selectListByApplicationId(Long applicationId) { + return selectList(new LambdaQueryWrapperX() + .eq(OvertimeApplicationApprovalRecordDO::getOvertimeApplicationId, applicationId) + .orderByDesc(OvertimeApplicationApprovalRecordDO::getApprovalRound) + .orderByDesc(OvertimeApplicationApprovalRecordDO::getId)); + } + + default int countByApplicationId(Long applicationId) { + return Math.toIntExact(selectCount(new LambdaQueryWrapperX() + .eq(OvertimeApplicationApprovalRecordDO::getOvertimeApplicationId, applicationId))); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/personal/PersonalItemMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/personal/PersonalItemMapper.java index 321c6e7..b06e746 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/personal/PersonalItemMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/personal/PersonalItemMapper.java @@ -9,6 +9,9 @@ import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO; import org.apache.ibatis.annotations.Mapper; import org.springframework.util.StringUtils; +import java.time.LocalDate; +import java.util.List; + @Mapper public interface PersonalItemMapper extends BaseMapperX { @@ -45,4 +48,33 @@ public interface PersonalItemMapper extends BaseMapperX { .eq(PersonalItemDO::getId, id) .eq(PersonalItemDO::getStatusCode, fromStatus)); } + + default List selectPlannedListByOwnerIdAndOverlap(Long ownerId, LocalDate startDate, LocalDate endDate, + String excludedStatusCode) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .eq(PersonalItemDO::getOwnerId, ownerId); + if (StringUtils.hasText(excludedStatusCode)) { + queryWrapper.ne(PersonalItemDO::getStatusCode, excludedStatusCode); + } + queryWrapper.isNotNull(PersonalItemDO::getPlannedStartDate); + queryWrapper.isNotNull(PersonalItemDO::getPlannedEndDate); + queryWrapper.le(PersonalItemDO::getPlannedStartDate, endDate); + queryWrapper.ge(PersonalItemDO::getPlannedEndDate, startDate); + queryWrapper.orderByAsc(PersonalItemDO::getPlannedStartDate); + queryWrapper.orderByAsc(PersonalItemDO::getId); + return selectList(queryWrapper); + } + + default List selectListByOwnerIdAndStatusNot(Long ownerId, String excludedStatusCode) { + if (ownerId == null) { + return List.of(); + } + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .eq(PersonalItemDO::getOwnerId, ownerId); + if (StringUtils.hasText(excludedStatusCode)) { + queryWrapper.ne(PersonalItemDO::getStatusCode, excludedStatusCode); + } + queryWrapper.orderByAsc(PersonalItemDO::getId); + return selectList(queryWrapper); + } } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java index edf9fbf..8d8c0f5 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java @@ -58,6 +58,12 @@ public interface ProjectMapper extends BaseMapperX { .orderByDesc(BaseDO::getCreateTime)); } + default List selectListByManagerUserId(Long managerUserId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProjectDO::getManagerUserId, managerUserId) + .orderByDesc(BaseDO::getCreateTime)); + } + default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) { ProjectDO update = new ProjectDO(); update.setStatusCode(toStatus); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java index 6d8b574..1d82ea7 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java @@ -12,6 +12,7 @@ import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExec import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; +import org.springframework.util.StringUtils; import java.time.LocalDate; import java.util.Collection; @@ -143,18 +144,6 @@ public interface ProjectExecutionMapper extends BaseMapperX return Math.toIntExact(selectCount(queryWrapper)); } - /** - * 统计指定项目下处于非终态的执行数。用于项目 complete 前置校验(TD-015)。 - */ - default Integer countNonTerminalByProjectId(Long projectId, List terminalStatusCodes) { - LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() - .eq(ProjectExecutionDO::getProjectId, projectId); - if (terminalStatusCodes != null && !terminalStatusCodes.isEmpty()) { - queryWrapper.notIn(ProjectExecutionDO::getStatusCode, terminalStatusCodes); - } - return Math.toIntExact(selectCount(queryWrapper)); - } - default Integer countByProjectIdAndStatusCode(Long projectId, ProjectExecutionStatusBoardReqVO reqVO, String statusCode, @@ -312,4 +301,45 @@ public interface ProjectExecutionMapper extends BaseMapperX @Param("projectIds") Collection projectIds, @Param("terminalStatusCodes") Collection terminalStatusCodes); + default List selectListByProjectId(Long projectId) { + if (projectId == null) { + return java.util.Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .eq(ProjectExecutionDO::getProjectId, projectId) + .orderByAsc(ProjectExecutionDO::getId)); + } + + default List selectPlannedListByProjectIdAndOverlap(Long projectId, LocalDate startDate, + LocalDate endDate, String excludedStatusCode) { + if (projectId == null || startDate == null || endDate == null) { + return java.util.Collections.emptyList(); + } + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .eq(ProjectExecutionDO::getProjectId, projectId); + if (StringUtils.hasText(excludedStatusCode)) { + queryWrapper.ne(ProjectExecutionDO::getStatusCode, excludedStatusCode); + } + queryWrapper.isNotNull(ProjectExecutionDO::getPlannedStartDate); + queryWrapper.isNotNull(ProjectExecutionDO::getPlannedEndDate); + queryWrapper.le(ProjectExecutionDO::getPlannedStartDate, endDate); + queryWrapper.ge(ProjectExecutionDO::getPlannedEndDate, startDate); + queryWrapper.orderByAsc(ProjectExecutionDO::getPlannedStartDate); + queryWrapper.orderByAsc(ProjectExecutionDO::getId); + return selectList(queryWrapper); + } + + default List selectListByProjectIdAndStatusNot(Long projectId, String excludedStatusCode) { + if (projectId == null) { + return java.util.Collections.emptyList(); + } + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .eq(ProjectExecutionDO::getProjectId, projectId); + if (StringUtils.hasText(excludedStatusCode)) { + queryWrapper.ne(ProjectExecutionDO::getStatusCode, excludedStatusCode); + } + queryWrapper.orderByAsc(ProjectExecutionDO::getId); + return selectList(queryWrapper); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java index f997fd4..e518fa6 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java @@ -791,4 +791,72 @@ public interface ProjectTaskMapper extends BaseMapperX { @Param("projectIds") Collection projectIds, @Param("terminalStatusCodes") Collection terminalStatusCodes); + default List selectListByProjectId(Long projectId) { + if (projectId == null) { + return List.of(); + } + return selectList(new LambdaQueryWrapperX() + .eq(ProjectTaskDO::getProjectId, projectId) + .orderByAsc(ProjectTaskDO::getExecutionId) + .orderByAsc(ProjectTaskDO::getId)); + } + + @Select(""" + + """) + List selectPlannedInvolvedListByUserIdAndOverlap(@Param("userId") Long userId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("excludedStatusCode") String excludedStatusCode); + + @Select(""" + + """) + List selectInvolvedListByUserIdAndStatusNot(@Param("userId") Long userId, + @Param("excludedStatusCode") String excludedStatusCode); + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java index 4b69791..4998d60 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java @@ -188,4 +188,30 @@ public interface TaskWorklogMapper extends BaseMapperX { .eq(TaskWorklogDO::getTaskId, taskId))); } + default List selectListByUserIdAndPeriod(Long userId, LocalDate startDate, LocalDate endDate) { + if (userId == null || startDate == null || endDate == null) { + return List.of(); + } + return selectList(new LambdaQueryWrapperX() + .eq(TaskWorklogDO::getUserId, userId) + .le(TaskWorklogDO::getStartDate, endDate) + .ge(TaskWorklogDO::getEndDate, startDate) + .orderByAsc(TaskWorklogDO::getTaskId) + .orderByAsc(TaskWorklogDO::getEndDate) + .orderByAsc(TaskWorklogDO::getId)); + } + + default List selectListByTaskIdsAndPeriod(Collection taskIds, LocalDate startDate, LocalDate endDate) { + if (taskIds == null || taskIds.isEmpty() || startDate == null || endDate == null) { + return List.of(); + } + return selectList(new LambdaQueryWrapperX() + .in(TaskWorklogDO::getTaskId, taskIds) + .le(TaskWorklogDO::getStartDate, endDate) + .ge(TaskWorklogDO::getEndDate, startDate) + .orderByAsc(TaskWorklogDO::getTaskId) + .orderByAsc(TaskWorklogDO::getEndDate) + .orderByAsc(TaskWorklogDO::getId)); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/PersonalReportPlanItemMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/PersonalReportPlanItemMapper.java new file mode 100644 index 0000000..a1d50f8 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/PersonalReportPlanItemMapper.java @@ -0,0 +1,26 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.common; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.PersonalReportPlanItemDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PersonalReportPlanItemMapper extends BaseMapperX { + + default List selectListByReport(String reportType, Long reportId) { + return selectList(new LambdaQueryWrapperX() + .eq(PersonalReportPlanItemDO::getReportType, reportType) + .eq(PersonalReportPlanItemDO::getReportId, reportId) + .orderByAsc(PersonalReportPlanItemDO::getItemNumber) + .orderByAsc(PersonalReportPlanItemDO::getId)); + } + + default int deleteByReport(String reportType, Long reportId) { + return delete(new LambdaQueryWrapperX() + .eq(PersonalReportPlanItemDO::getReportType, reportType) + .eq(PersonalReportPlanItemDO::getReportId, reportId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/PersonalReportReviewItemMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/PersonalReportReviewItemMapper.java new file mode 100644 index 0000000..b637394 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/PersonalReportReviewItemMapper.java @@ -0,0 +1,26 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.common; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.PersonalReportReviewItemDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PersonalReportReviewItemMapper extends BaseMapperX { + + default List selectListByReport(String reportType, Long reportId) { + return selectList(new LambdaQueryWrapperX() + .eq(PersonalReportReviewItemDO::getReportType, reportType) + .eq(PersonalReportReviewItemDO::getReportId, reportId) + .orderByAsc(PersonalReportReviewItemDO::getItemNumber) + .orderByAsc(PersonalReportReviewItemDO::getId)); + } + + default int deleteByReport(String reportType, Long reportId) { + return delete(new LambdaQueryWrapperX() + .eq(PersonalReportReviewItemDO::getReportType, reportType) + .eq(PersonalReportReviewItemDO::getReportId, reportId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/WorkReportStatusLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/WorkReportStatusLogMapper.java new file mode 100644 index 0000000..23b41b9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/common/WorkReportStatusLogMapper.java @@ -0,0 +1,26 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.common; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportStatusLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface WorkReportStatusLogMapper extends BaseMapperX { + + default List selectListByReport(String reportType, Long reportId) { + return selectList(new LambdaQueryWrapperX() + .eq(WorkReportStatusLogDO::getReportType, reportType) + .eq(WorkReportStatusLogDO::getReportId, reportId) + .orderByDesc(WorkReportStatusLogDO::getCreateTime) + .orderByDesc(WorkReportStatusLogDO::getId)); + } + + default int deleteByReport(String reportType, Long reportId) { + return delete(new LambdaQueryWrapperX() + .eq(WorkReportStatusLogDO::getReportType, reportType) + .eq(WorkReportStatusLogDO::getReportId, reportId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportApprovalRecordMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportApprovalRecordMapper.java new file mode 100644 index 0000000..a1924de --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportApprovalRecordMapper.java @@ -0,0 +1,29 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.monthly; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportApprovalRecordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MonthlyReportApprovalRecordMapper extends BaseMapperX { + + default List selectListByMonthlyReportId(Long monthlyReportId) { + return selectList(new LambdaQueryWrapperX() + .eq(MonthlyReportApprovalRecordDO::getMonthlyReportId, monthlyReportId) + .orderByDesc(MonthlyReportApprovalRecordDO::getApprovalRound) + .orderByDesc(MonthlyReportApprovalRecordDO::getId)); + } + + default int countByMonthlyReportId(Long monthlyReportId) { + return Math.toIntExact(selectCount(new LambdaQueryWrapperX() + .eq(MonthlyReportApprovalRecordDO::getMonthlyReportId, monthlyReportId))); + } + + default int deleteByMonthlyReportId(Long monthlyReportId) { + return delete(new LambdaQueryWrapperX() + .eq(MonthlyReportApprovalRecordDO::getMonthlyReportId, monthlyReportId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportMapper.java new file mode 100644 index 0000000..01f0732 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportMapper.java @@ -0,0 +1,123 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.monthly; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportPageReqVO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportDO; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface MonthlyReportMapper extends BaseMapperX { + + default MonthlyReportDO selectByReporterIdAndPeriodKey(Long reporterId, String periodKey) { + return selectOne(new LambdaQueryWrapperX() + .eq(MonthlyReportDO::getReporterId, reporterId) + .eq(MonthlyReportDO::getPeriodKey, periodKey)); + } + + default PageResult selectReporterPage(Long reporterId, MonthlyReportPageReqVO reqVO) { + return selectReporterPage(reporterId, reqVO, null); + } + + default PageResult selectReporterPage(Long reporterId, MonthlyReportPageReqVO reqVO, + Collection allowedStatusCodes) { + if (allowedStatusCodes != null && allowedStatusCodes.isEmpty()) { + return new PageResult<>(List.of(), 0L); + } + LambdaQueryWrapperX wrapper = buildPageQuery(reqVO) + .eq(MonthlyReportDO::getReporterId, reporterId) + .orderByDesc(MonthlyReportDO::getPeriodStartDate) + .orderByDesc(MonthlyReportDO::getId); + if (allowedStatusCodes != null) { + wrapper.in(MonthlyReportDO::getStatusCode, allowedStatusCodes); + } + return selectPage(reqVO, wrapper); + } + + default PageResult selectApprovalPage(Long supervisorUserId, MonthlyReportPageReqVO reqVO) { + LambdaQueryWrapperX wrapper = buildPageQuery(reqVO) + .eq(MonthlyReportDO::getSupervisorUserId, supervisorUserId) + .eq(MonthlyReportDO::getStatusCode, WorkReportConstants.STATUS_PENDING_APPROVAL) + .orderByDesc(MonthlyReportDO::getSubmitTime) + .orderByDesc(MonthlyReportDO::getId); + return selectPage(reqVO, wrapper); + } + + default int updateByIdAndStatus(MonthlyReportDO update, Long id, String fromStatus) { + return update(update, new LambdaQueryWrapperX() + .eq(MonthlyReportDO::getId, id) + .eq(MonthlyReportDO::getStatusCode, fromStatus)); + } + + default int updateSubmitFieldsByIdAndStatus(Long id, String fromStatus, String reporterDeptName, + String reporterPostName, Long supervisorUserId, + String supervisorName, String toStatus, LocalDateTime submitTime, + String updater) { + return update(null, new LambdaUpdateWrapper() + .set(MonthlyReportDO::getReporterDeptName, reporterDeptName) + .set(MonthlyReportDO::getReporterPostName, reporterPostName) + .set(MonthlyReportDO::getSupervisorUserId, supervisorUserId) + .set(MonthlyReportDO::getSupervisorName, supervisorName) + .set(MonthlyReportDO::getStatusCode, toStatus) + .set(MonthlyReportDO::getSubmitTime, submitTime) + .set(MonthlyReportDO::getApprovalTime, null) + .set(MonthlyReportDO::getApprovalComment, null) + .set(MonthlyReportDO::getLastStatusReason, null) + .set(MonthlyReportDO::getUpdateTime, submitTime) + .set(MonthlyReportDO::getUpdater, updater) + .eq(MonthlyReportDO::getId, id) + .eq(MonthlyReportDO::getStatusCode, fromStatus)); + } + + default int updateApprovalFieldsByIdAndStatus(Long id, String fromStatus, String toStatus, + LocalDateTime approvalTime, String approvalComment, + String lastStatusReason, String updater) { + return update(null, new LambdaUpdateWrapper() + .set(MonthlyReportDO::getStatusCode, toStatus) + .set(MonthlyReportDO::getApprovalTime, approvalTime) + .set(MonthlyReportDO::getApprovalComment, approvalComment) + .set(MonthlyReportDO::getLastStatusReason, lastStatusReason) + .set(MonthlyReportDO::getUpdateTime, approvalTime) + .set(MonthlyReportDO::getUpdater, updater) + .eq(MonthlyReportDO::getId, id) + .eq(MonthlyReportDO::getStatusCode, fromStatus)); + } + + default int updateByIdAndStatusesAndReporterId(MonthlyReportDO update, Long id, Collection statuses, + Long reporterId) { + return update(update, new LambdaQueryWrapperX() + .eq(MonthlyReportDO::getId, id) + .eq(MonthlyReportDO::getReporterId, reporterId) + .in(MonthlyReportDO::getStatusCode, statuses)); + } + + default int deleteByIdAndStatusesAndReporterId(Long id, Collection statuses, Long reporterId) { + return delete(new LambdaQueryWrapperX() + .eq(MonthlyReportDO::getId, id) + .eq(MonthlyReportDO::getReporterId, reporterId) + .in(MonthlyReportDO::getStatusCode, statuses)); + } + + private LambdaQueryWrapperX buildPageQuery(MonthlyReportPageReqVO reqVO) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eqIfPresent(MonthlyReportDO::getStatusCode, reqVO.getStatusCode()) + .betweenIfPresent(MonthlyReportDO::getPeriodStartDate, reqVO.getPeriodStartDate()) + .betweenIfPresent(MonthlyReportDO::getSubmitTime, reqVO.getSubmitTime()); + if (StringUtils.hasText(reqVO.getKeyword())) { + wrapper.and(w -> w.like(MonthlyReportDO::getPeriodLabel, reqVO.getKeyword()) + .or() + .like(MonthlyReportDO::getReporterName, reqVO.getKeyword()) + .or() + .like(MonthlyReportDO::getSupervisorName, reqVO.getKeyword())); + } + return wrapper; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportApprovalRecordMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportApprovalRecordMapper.java new file mode 100644 index 0000000..ea5ddcd --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportApprovalRecordMapper.java @@ -0,0 +1,29 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.project; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportApprovalRecordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ProjectReportApprovalRecordMapper extends BaseMapperX { + + default List selectListByProjectReportId(Long projectReportId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProjectReportApprovalRecordDO::getProjectReportId, projectReportId) + .orderByDesc(ProjectReportApprovalRecordDO::getApprovalRound) + .orderByDesc(ProjectReportApprovalRecordDO::getId)); + } + + default int countByProjectReportId(Long projectReportId) { + return Math.toIntExact(selectCount(new LambdaQueryWrapperX() + .eq(ProjectReportApprovalRecordDO::getProjectReportId, projectReportId))); + } + + default int deleteByProjectReportId(Long projectReportId) { + return delete(new LambdaQueryWrapperX() + .eq(ProjectReportApprovalRecordDO::getProjectReportId, projectReportId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportCurrentItemMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportCurrentItemMapper.java new file mode 100644 index 0000000..b1ba562 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportCurrentItemMapper.java @@ -0,0 +1,23 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.project; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportCurrentItemDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ProjectReportCurrentItemMapper extends BaseMapperX { + + default List selectListByReportId(Long reportId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProjectReportCurrentItemDO::getReportId, reportId) + .orderByAsc(ProjectReportCurrentItemDO::getId)); + } + + default int deleteByReportId(Long reportId) { + return delete(new LambdaQueryWrapperX() + .eq(ProjectReportCurrentItemDO::getReportId, reportId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportMapper.java new file mode 100644 index 0000000..7281a1d --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportMapper.java @@ -0,0 +1,136 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.project; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportPageReqVO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportMemberSnapshotItem; +import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportDO; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface ProjectReportMapper extends BaseMapperX { + + String JACKSON_TYPE_HANDLER_MAPPING = "typeHandler=" + JacksonTypeHandler.class.getCanonicalName(); + + default ProjectReportDO selectByProjectIdAndPeriodKeyAndProjectOwnerId(Long projectId, String periodKey, + Long projectOwnerId) { + return selectOne(new LambdaQueryWrapperX() + .eq(ProjectReportDO::getProjectId, projectId) + .eq(ProjectReportDO::getPeriodKey, periodKey) + .eq(ProjectReportDO::getProjectOwnerId, projectOwnerId)); + } + + default PageResult selectReporterPage(Long reporterId, ProjectReportPageReqVO reqVO) { + return selectReporterPage(reporterId, reqVO, null); + } + + default PageResult selectReporterPage(Long reporterId, ProjectReportPageReqVO reqVO, + Collection allowedStatusCodes) { + if (allowedStatusCodes != null && allowedStatusCodes.isEmpty()) { + return new PageResult<>(List.of(), 0L); + } + LambdaQueryWrapperX wrapper = buildPageQuery(reqVO) + .eq(ProjectReportDO::getProjectOwnerId, reporterId) + .orderByDesc(ProjectReportDO::getPeriodStartDate) + .orderByDesc(ProjectReportDO::getId); + if (allowedStatusCodes != null) { + wrapper.in(ProjectReportDO::getStatusCode, allowedStatusCodes); + } + return selectPage(reqVO, wrapper); + } + + default PageResult selectApprovalPage(Long supervisorUserId, ProjectReportPageReqVO reqVO) { + LambdaQueryWrapperX wrapper = buildPageQuery(reqVO) + .eq(ProjectReportDO::getSupervisorUserId, supervisorUserId) + .eq(ProjectReportDO::getStatusCode, WorkReportConstants.STATUS_PENDING_APPROVAL) + .orderByDesc(ProjectReportDO::getSubmitTime) + .orderByDesc(ProjectReportDO::getId); + return selectPage(reqVO, wrapper); + } + + default int updateByIdAndStatus(ProjectReportDO update, Long id, String fromStatus) { + return update(update, new LambdaQueryWrapperX() + .eq(ProjectReportDO::getId, id) + .eq(ProjectReportDO::getStatusCode, fromStatus)); + } + + default int updateSubmitFieldsByIdAndStatus(Long id, String fromStatus, String projectName, + Long projectOwnerId, String projectOwnerName, + List projectMemberSnapshot, + Long supervisorUserId, String supervisorName, String toStatus, + LocalDateTime submitTime, String updater) { + return update(null, new LambdaUpdateWrapper() + .set(ProjectReportDO::getProjectName, projectName) + .set(ProjectReportDO::getProjectOwnerId, projectOwnerId) + .set(ProjectReportDO::getProjectOwnerName, projectOwnerName) + .set(ProjectReportDO::getProjectMemberSnapshot, projectMemberSnapshot, JACKSON_TYPE_HANDLER_MAPPING) + .set(ProjectReportDO::getSupervisorUserId, supervisorUserId) + .set(ProjectReportDO::getSupervisorName, supervisorName) + .set(ProjectReportDO::getStatusCode, toStatus) + .set(ProjectReportDO::getSubmitTime, submitTime) + .set(ProjectReportDO::getApprovalTime, null) + .set(ProjectReportDO::getApprovalComment, null) + .set(ProjectReportDO::getLastStatusReason, null) + .set(ProjectReportDO::getUpdateTime, submitTime) + .set(ProjectReportDO::getUpdater, updater) + .eq(ProjectReportDO::getId, id) + .eq(ProjectReportDO::getStatusCode, fromStatus)); + } + + default int updateApprovalFieldsByIdAndStatus(Long id, String fromStatus, String toStatus, + LocalDateTime approvalTime, String approvalComment, + String lastStatusReason, String updater) { + return update(null, new LambdaUpdateWrapper() + .set(ProjectReportDO::getStatusCode, toStatus) + .set(ProjectReportDO::getApprovalTime, approvalTime) + .set(ProjectReportDO::getApprovalComment, approvalComment) + .set(ProjectReportDO::getLastStatusReason, lastStatusReason) + .set(ProjectReportDO::getUpdateTime, approvalTime) + .set(ProjectReportDO::getUpdater, updater) + .eq(ProjectReportDO::getId, id) + .eq(ProjectReportDO::getStatusCode, fromStatus)); + } + + default int updateByIdAndStatusesAndReporterId(ProjectReportDO update, Long id, Collection statuses, + Long reporterId) { + return update(update, new LambdaQueryWrapperX() + .eq(ProjectReportDO::getId, id) + .eq(ProjectReportDO::getProjectOwnerId, reporterId) + .in(ProjectReportDO::getStatusCode, statuses)); + } + + default int deleteByIdAndStatusesAndReporterId(Long id, Collection statuses, Long reporterId) { + return delete(new LambdaQueryWrapperX() + .eq(ProjectReportDO::getId, id) + .eq(ProjectReportDO::getProjectOwnerId, reporterId) + .in(ProjectReportDO::getStatusCode, statuses)); + } + + private LambdaQueryWrapperX buildPageQuery(ProjectReportPageReqVO reqVO) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eqIfPresent(ProjectReportDO::getProjectId, reqVO.getProjectId()) + .eqIfPresent(ProjectReportDO::getFlag, reqVO.getFlag()) + .eqIfPresent(ProjectReportDO::getStatusCode, reqVO.getStatusCode()) + .betweenIfPresent(ProjectReportDO::getPeriodStartDate, reqVO.getPeriodStartDate()) + .betweenIfPresent(ProjectReportDO::getSubmitTime, reqVO.getSubmitTime()); + if (StringUtils.hasText(reqVO.getKeyword())) { + wrapper.and(w -> w.like(ProjectReportDO::getProjectName, reqVO.getKeyword()) + .or() + .like(ProjectReportDO::getPeriodLabel, reqVO.getKeyword()) + .or() + .like(ProjectReportDO::getProjectOwnerName, reqVO.getKeyword()) + .or() + .like(ProjectReportDO::getSupervisorName, reqVO.getKeyword())); + } + return wrapper; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportNextItemMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportNextItemMapper.java new file mode 100644 index 0000000..6542f10 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/project/ProjectReportNextItemMapper.java @@ -0,0 +1,23 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.project; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportNextItemDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ProjectReportNextItemMapper extends BaseMapperX { + + default List selectListByReportId(Long reportId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProjectReportNextItemDO::getReportId, reportId) + .orderByAsc(ProjectReportNextItemDO::getId)); + } + + default int deleteByReportId(Long reportId) { + return delete(new LambdaQueryWrapperX() + .eq(ProjectReportNextItemDO::getReportId, reportId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportApprovalRecordMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportApprovalRecordMapper.java new file mode 100644 index 0000000..2bd8c4b --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportApprovalRecordMapper.java @@ -0,0 +1,29 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.weekly; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportApprovalRecordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface WeeklyReportApprovalRecordMapper extends BaseMapperX { + + default List selectListByWeeklyReportId(Long weeklyReportId) { + return selectList(new LambdaQueryWrapperX() + .eq(WeeklyReportApprovalRecordDO::getWeeklyReportId, weeklyReportId) + .orderByDesc(WeeklyReportApprovalRecordDO::getApprovalRound) + .orderByDesc(WeeklyReportApprovalRecordDO::getId)); + } + + default int countByWeeklyReportId(Long weeklyReportId) { + return Math.toIntExact(selectCount(new LambdaQueryWrapperX() + .eq(WeeklyReportApprovalRecordDO::getWeeklyReportId, weeklyReportId))); + } + + default int deleteByWeeklyReportId(Long weeklyReportId) { + return delete(new LambdaQueryWrapperX() + .eq(WeeklyReportApprovalRecordDO::getWeeklyReportId, weeklyReportId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportMapper.java new file mode 100644 index 0000000..cd05f10 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportMapper.java @@ -0,0 +1,124 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.weekly; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportPageReqVO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportDO; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface WeeklyReportMapper extends BaseMapperX { + + default WeeklyReportDO selectByReporterIdAndPeriodKey(Long reporterId, String periodKey) { + return selectOne(new LambdaQueryWrapperX() + .eq(WeeklyReportDO::getReporterId, reporterId) + .eq(WeeklyReportDO::getPeriodKey, periodKey)); + } + + default PageResult selectReporterPage(Long reporterId, WeeklyReportPageReqVO reqVO) { + return selectReporterPage(reporterId, reqVO, null); + } + + default PageResult selectReporterPage(Long reporterId, WeeklyReportPageReqVO reqVO, + Collection allowedStatusCodes) { + if (allowedStatusCodes != null && allowedStatusCodes.isEmpty()) { + return new PageResult<>(List.of(), 0L); + } + LambdaQueryWrapperX wrapper = buildPageQuery(reqVO) + .eq(WeeklyReportDO::getReporterId, reporterId) + .orderByDesc(WeeklyReportDO::getPeriodStartDate) + .orderByDesc(WeeklyReportDO::getId); + if (allowedStatusCodes != null) { + wrapper.in(WeeklyReportDO::getStatusCode, allowedStatusCodes); + } + return selectPage(reqVO, wrapper); + } + + default PageResult selectApprovalPage(Long supervisorUserId, WeeklyReportPageReqVO reqVO) { + LambdaQueryWrapperX wrapper = buildPageQuery(reqVO) + .eq(WeeklyReportDO::getSupervisorUserId, supervisorUserId) + .eq(WeeklyReportDO::getStatusCode, WorkReportConstants.STATUS_PENDING_APPROVAL) + .orderByDesc(WeeklyReportDO::getSubmitTime) + .orderByDesc(WeeklyReportDO::getId); + return selectPage(reqVO, wrapper); + } + + default int updateByIdAndStatus(WeeklyReportDO update, Long id, String fromStatus) { + return update(update, new LambdaQueryWrapperX() + .eq(WeeklyReportDO::getId, id) + .eq(WeeklyReportDO::getStatusCode, fromStatus)); + } + + default int updateSubmitFieldsByIdAndStatus(Long id, String fromStatus, String reporterDeptName, + String reporterPostName, Long supervisorUserId, + String supervisorName, String toStatus, LocalDateTime submitTime, + String updater) { + return update(null, new LambdaUpdateWrapper() + .set(WeeklyReportDO::getReporterDeptName, reporterDeptName) + .set(WeeklyReportDO::getReporterPostName, reporterPostName) + .set(WeeklyReportDO::getSupervisorUserId, supervisorUserId) + .set(WeeklyReportDO::getSupervisorName, supervisorName) + .set(WeeklyReportDO::getStatusCode, toStatus) + .set(WeeklyReportDO::getSubmitTime, submitTime) + .set(WeeklyReportDO::getApprovalTime, null) + .set(WeeklyReportDO::getApprovalComment, null) + .set(WeeklyReportDO::getLastStatusReason, null) + .set(WeeklyReportDO::getUpdateTime, submitTime) + .set(WeeklyReportDO::getUpdater, updater) + .eq(WeeklyReportDO::getId, id) + .eq(WeeklyReportDO::getStatusCode, fromStatus)); + } + + default int updateApprovalFieldsByIdAndStatus(Long id, String fromStatus, String toStatus, + LocalDateTime approvalTime, String approvalComment, + String lastStatusReason, String updater) { + return update(null, new LambdaUpdateWrapper() + .set(WeeklyReportDO::getStatusCode, toStatus) + .set(WeeklyReportDO::getApprovalTime, approvalTime) + .set(WeeklyReportDO::getApprovalComment, approvalComment) + .set(WeeklyReportDO::getLastStatusReason, lastStatusReason) + .set(WeeklyReportDO::getUpdateTime, approvalTime) + .set(WeeklyReportDO::getUpdater, updater) + .eq(WeeklyReportDO::getId, id) + .eq(WeeklyReportDO::getStatusCode, fromStatus)); + } + + default int updateByIdAndStatusesAndReporterId(WeeklyReportDO update, Long id, Collection statuses, + Long reporterId) { + return update(update, new LambdaQueryWrapperX() + .eq(WeeklyReportDO::getId, id) + .eq(WeeklyReportDO::getReporterId, reporterId) + .in(WeeklyReportDO::getStatusCode, statuses)); + } + + default int deleteByIdAndStatusesAndReporterId(Long id, Collection statuses, Long reporterId) { + return delete(new LambdaQueryWrapperX() + .eq(WeeklyReportDO::getId, id) + .eq(WeeklyReportDO::getReporterId, reporterId) + .in(WeeklyReportDO::getStatusCode, statuses)); + } + + private LambdaQueryWrapperX buildPageQuery(WeeklyReportPageReqVO reqVO) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eqIfPresent(WeeklyReportDO::getStatusCode, reqVO.getStatusCode()) + .eqIfPresent(WeeklyReportDO::getIsBusinessTrip, reqVO.getIsBusinessTrip()) + .betweenIfPresent(WeeklyReportDO::getPeriodStartDate, reqVO.getPeriodStartDate()) + .betweenIfPresent(WeeklyReportDO::getSubmitTime, reqVO.getSubmitTime()); + if (StringUtils.hasText(reqVO.getKeyword())) { + wrapper.and(w -> w.like(WeeklyReportDO::getPeriodLabel, reqVO.getKeyword()) + .or() + .like(WeeklyReportDO::getReporterName, reqVO.getKeyword()) + .or() + .like(WeeklyReportDO::getSupervisorName, reqVO.getKeyword())); + } + return wrapper; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportTravelSegmentMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportTravelSegmentMapper.java new file mode 100644 index 0000000..ebf543c --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/weekly/WeeklyReportTravelSegmentMapper.java @@ -0,0 +1,24 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.weekly; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportTravelSegmentDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface WeeklyReportTravelSegmentMapper extends BaseMapperX { + + default List selectListByWeeklyReportId(Long weeklyReportId) { + return selectList(new LambdaQueryWrapperX() + .eq(WeeklyReportTravelSegmentDO::getWeeklyReportId, weeklyReportId) + .orderByAsc(WeeklyReportTravelSegmentDO::getSort) + .orderByAsc(WeeklyReportTravelSegmentDO::getId)); + } + + default int deleteByWeeklyReportId(Long weeklyReportId) { + return delete(new LambdaQueryWrapperX() + .eq(WeeklyReportTravelSegmentDO::getWeeklyReportId, weeklyReportId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java index 986acf8..a5acaac 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java @@ -1,13 +1,15 @@ package com.njcn.rdms.module.project.framework.rpc.config; +import com.njcn.rdms.module.system.api.dept.DeptApi; import com.njcn.rdms.module.system.api.dept.OrgLeaderApi; +import com.njcn.rdms.module.system.api.dept.PostApi; import com.njcn.rdms.module.system.api.dict.DictDataApi; import com.njcn.rdms.module.system.api.file.FileApi; -import com.njcn.rdms.module.system.api.notify.NotifyMessageSendApi; import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; import com.njcn.rdms.module.system.api.permission.PermissionApi; import com.njcn.rdms.module.system.api.permission.UserVisibilityConfigApi; import com.njcn.rdms.module.system.api.user.AdminUserApi; +import com.njcn.rdms.module.system.api.user.UserManagementRelationApi; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; @@ -15,6 +17,9 @@ import org.springframework.context.annotation.Configuration; * Project 模块的 RPC 配置 */ @Configuration(value = "projectRpcConfiguration", proxyBeanMethods = false) -@EnableFeignClients(clients = {AdminUserApi.class, ObjectPermissionApi.class, DictDataApi.class, FileApi.class, PermissionApi.class, OrgLeaderApi.class, UserVisibilityConfigApi.class, NotifyMessageSendApi.class}) +@EnableFeignClients(clients = + {AdminUserApi.class, ObjectPermissionApi.class, DictDataApi.class, FileApi.class, + PermissionApi.class, OrgLeaderApi.class, UserVisibilityConfigApi.class, + DeptApi.class, PostApi.class, UserManagementRelationApi.class}) public class RpcConfiguration { } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationService.java index d30aa05..c67cc10 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationService.java @@ -1,6 +1,7 @@ package com.njcn.rdms.module.project.service.overtime; import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationApprovalRecordRespVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO; @@ -21,8 +22,6 @@ public interface OvertimeApplicationService { void reject(Long id, OvertimeApplicationStatusActionReqVO reqVO); - void cancel(Long id, OvertimeApplicationStatusActionReqVO reqVO); - void deleteApplication(Long id); OvertimeApplicationRespVO getApplication(Long id); @@ -35,5 +34,7 @@ public interface OvertimeApplicationService { List getStatusLogs(Long id); + List getApprovalRecords(Long id); + List getExportList(OvertimeApplicationPageReqVO reqVO); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationServiceImpl.java index 7d6d479..6b165b1 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationServiceImpl.java @@ -7,6 +7,7 @@ import com.njcn.rdms.framework.common.util.json.JsonUtils; import com.njcn.rdms.framework.common.util.object.BeanUtils; import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants; +import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationApprovalRecordRespVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO; @@ -15,11 +16,13 @@ import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplica import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusDictRespVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO; import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO; +import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationApprovalRecordDO; import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO; import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationStatusLogDO; import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO; import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO; import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper; +import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationApprovalRecordMapper; import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationMapper; import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationStatusLogMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; @@ -53,6 +56,8 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic @Resource private OvertimeApplicationStatusLogMapper overtimeApplicationStatusLogMapper; @Resource + private OvertimeApplicationApprovalRecordMapper overtimeApplicationApprovalRecordMapper; + @Resource private BizAuditLogMapper bizAuditLogMapper; @Resource private ObjectStatusModelMapper objectStatusModelMapper; @@ -108,8 +113,7 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic update.setApprovalTime(null); int updateCount = overtimeApplicationMapper.updateByIdAndStatusesAndApplicantId(update, id, - List.of(OvertimeApplicationConstants.STATUS_REJECTED, OvertimeApplicationConstants.STATUS_CANCELLED), - loginUserId); + List.of(OvertimeApplicationConstants.STATUS_REJECTED), loginUserId); if (updateCount != 1) { throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED); } @@ -133,36 +137,6 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic processApprovalAction(id, OvertimeApplicationConstants.ACTION_REJECT, reqVO); } - @Override - @Transactional(rollbackFor = Exception.class) - public void cancel(Long id, OvertimeApplicationStatusActionReqVO reqVO) { - Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); - OvertimeApplicationDO current = validateApplicationExists(id); - if (!Objects.equals(current.getApplicantId(), loginUserId)) { - throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY); - } - String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason()); - String fromStatus = current.getStatusCode(); - ObjectStatusTransitionDO transition = validateTransition(fromStatus, OvertimeApplicationConstants.ACTION_CANCEL, - reason); - - OvertimeApplicationDO update = new OvertimeApplicationDO(); - update.setStatusCode(transition.getToStatusCode()); - update.setApprovalComment(reason); - update.setApprovalTime(LocalDateTime.now()); - int updateCount = overtimeApplicationMapper.updateByIdAndStatusAndApplicantId(update, id, fromStatus, - loginUserId); - if (updateCount != 1) { - throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED); - } - - OvertimeApplicationDO after = mergeUpdated(current, update); - writeStatusLog(after, OvertimeApplicationConstants.ACTION_CANCEL, fromStatus, transition.getToStatusCode(), - reason); - writeAuditLog(after, OvertimeApplicationConstants.ACTION_CANCEL, fromStatus, transition.getToStatusCode(), - null, reason, null); - } - @Override @Transactional(rollbackFor = Exception.class) public void deleteApplication(Long id) { @@ -171,8 +145,8 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic if (!Objects.equals(current.getApplicantId(), loginUserId)) { throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY); } - if (!OvertimeApplicationConstants.STATUS_CANCELLED.equals(current.getStatusCode())) { - throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_DELETE_ONLY_CANCELLED); + if (!OvertimeApplicationConstants.STATUS_REJECTED.equals(current.getStatusCode())) { + throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_DELETE_ONLY_REJECTED); } overtimeApplicationMapper.deleteById(id); writeAuditLog(current, OvertimeApplicationConstants.ACTION_DELETE, current.getStatusCode(), null, null, null, @@ -215,6 +189,13 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic OvertimeApplicationStatusLogRespVO.class); } + @Override + public List getApprovalRecords(Long id) { + validateReadableApplication(id); + return BeanUtils.toBean(overtimeApplicationApprovalRecordMapper.selectListByApplicationId(id), + OvertimeApplicationApprovalRecordRespVO.class); + } + @Override public List getExportList(OvertimeApplicationPageReqVO reqVO) { reqVO.setPageSize(PageParam.PAGE_SIZE_NONE); @@ -242,7 +223,9 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic } OvertimeApplicationDO after = mergeUpdated(current, update); - writeStatusLog(after, actionCode, fromStatus, transition.getToStatusCode(), reason); + OvertimeApplicationStatusLogDO statusLog = writeStatusLog(after, actionCode, fromStatus, + transition.getToStatusCode(), reason); + writeApprovalRecord(after, statusLog, reason); writeAuditLog(after, actionCode, fromStatus, transition.getToStatusCode(), null, reason, null); } @@ -354,8 +337,8 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic return respVO; } - private void writeStatusLog(OvertimeApplicationDO application, String actionType, String fromStatus, - String toStatus, String reason) { + private OvertimeApplicationStatusLogDO writeStatusLog(OvertimeApplicationDO application, String actionType, + String fromStatus, String toStatus, String reason) { OvertimeApplicationStatusLogDO log = new OvertimeApplicationStatusLogDO(); log.setApplicationId(application.getId()); log.setActionType(actionType); @@ -369,6 +352,20 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic log.setOvertimeDurationSnapshot(application.getOvertimeDuration()); log.setRemark(buildSnapshotRemark(application)); overtimeApplicationStatusLogMapper.insert(log); + return log; + } + + private void writeApprovalRecord(OvertimeApplicationDO application, OvertimeApplicationStatusLogDO statusLog, + String reason) { + OvertimeApplicationApprovalRecordDO record = new OvertimeApplicationApprovalRecordDO(); + record.setOvertimeApplicationId(application.getId()); + record.setStatusLogId(statusLog.getId()); + record.setApprovalRound(overtimeApplicationApprovalRecordMapper.countByApplicationId(application.getId()) + 1); + record.setConclusion(statusLog.getToStatus()); + record.setOpinion(reason); + record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId()); + record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + overtimeApplicationApprovalRecordMapper.insert(record); } private void writeAuditLog(OvertimeApplicationDO application, String actionType, String fromStatus, diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java new file mode 100644 index 0000000..ec2323f --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java @@ -0,0 +1,1354 @@ +package com.njcn.rdms.module.project.service.workreport.common; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageParam; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; +import com.njcn.rdms.module.project.constant.ProjectObjectConstants; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.*; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.*; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.*; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.*; +import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO; +import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO; +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO; +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.PersonalReportPlanItemDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.PersonalReportReviewItemDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportMemberSnapshotItem; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportStatusLogDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportApprovalRecordDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportApprovalRecordDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportCurrentItemDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportNextItemDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportApprovalRecordDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportTravelSegmentDO; +import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper; +import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper; +import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.common.PersonalReportPlanItemMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.common.PersonalReportReviewItemMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.common.WorkReportStatusLogMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportApprovalRecordMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportApprovalRecordMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportCurrentItemMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportNextItemMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportApprovalRecordMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportTravelSegmentMapper; +import com.njcn.rdms.module.project.enums.ErrorCodeConstants; +import com.njcn.rdms.module.system.api.dept.DeptApi; +import com.njcn.rdms.module.system.api.dept.PostApi; +import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO; +import com.njcn.rdms.module.system.api.dept.dto.PostRespDTO; +import com.njcn.rdms.module.system.api.user.AdminUserApi; +import com.njcn.rdms.module.system.api.user.UserManagementRelationApi; +import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +@Service +public class WorkReportCommonService { + + private static final List ALLOW_DELETE_STATUSES = List.of( + WorkReportConstants.STATUS_DRAFT, + WorkReportConstants.STATUS_REJECTED); + + @Resource + private WeeklyReportMapper weeklyReportMapper; + @Resource + private MonthlyReportMapper monthlyReportMapper; + @Resource + private ProjectReportMapper projectReportMapper; + @Resource + private PersonalReportReviewItemMapper personalReportReviewItemMapper; + @Resource + private PersonalReportPlanItemMapper personalReportPlanItemMapper; + @Resource + private WeeklyReportTravelSegmentMapper weeklyReportTravelSegmentMapper; + @Resource + private ProjectReportCurrentItemMapper projectReportCurrentItemMapper; + @Resource + private ProjectReportNextItemMapper projectReportNextItemMapper; + @Resource + private WeeklyReportApprovalRecordMapper weeklyReportApprovalRecordMapper; + @Resource + private MonthlyReportApprovalRecordMapper monthlyReportApprovalRecordMapper; + @Resource + private ProjectReportApprovalRecordMapper projectReportApprovalRecordMapper; + @Resource + private WorkReportStatusLogMapper workReportStatusLogMapper; + @Resource + private BizAuditLogMapper bizAuditLogMapper; + @Resource + private ObjectStatusModelMapper objectStatusModelMapper; + @Resource + private ObjectStatusTransitionMapper objectStatusTransitionMapper; + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + @Resource + private PostApi postApi; + @Resource + private UserManagementRelationApi userManagementRelationApi; + @Resource + private ProjectMapper projectMapper; + @Resource + private UserObjectRoleMapper userObjectRoleMapper; + + public List getStatusDict() { + return objectStatusModelMapper.selectListByObjectTypeEnabled(WorkReportConstants.STATUS_OBJECT_TYPE).stream() + .map(this::toStatusDictRespVO) + .collect(Collectors.toList()); + } + + public List getProjectReportOwnerProjectOptions() { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + return projectMapper.selectListByManagerUserId(loginUserId).stream() + .map(project -> { + ProjectReportOwnerProjectOptionRespVO respVO = new ProjectReportOwnerProjectOptionRespVO(); + respVO.setId(project.getId()); + respVO.setProjectCode(project.getProjectCode()); + respVO.setProjectName(project.getProjectName()); + return respVO; + }) + .collect(Collectors.toList()); + } + + public void validateCurrentUserIsProjectReportProjectOwner(Long projectId) { + ProjectDO project = validateProjectExists(projectId); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + if (!Objects.equals(project.getManagerUserId(), loginUserId)) { + throw exception(ErrorCodeConstants.WORK_REPORT_PROJECT_OWNER_ONLY); + } + } + + public WeeklyReportRespVO initWeeklyReport() { + CurrentUserProfile profile = loadCurrentUserProfile(true); + WeeklyReportRespVO respVO = new WeeklyReportRespVO(); + fillPersonalBase(respVO, profile); + applyStatusView(respVO, getInitialStatusModel()); + respVO.setIsBusinessTrip(false); + respVO.setReviewItems(Collections.emptyList()); + respVO.setPlanItems(Collections.emptyList()); + respVO.setTravelSegments(Collections.emptyList()); + return respVO; + } + + @Transactional(rollbackFor = Exception.class) + public Long createWeeklyReport(WeeklyReportSaveReqVO reqVO) { + validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + CurrentUserProfile profile = loadCurrentUserProfile(true); + validateWeeklyDuplicate(profile.userId(), reqVO.getPeriodKey(), null); + + WeeklyReportDO report = new WeeklyReportDO(); + applyWeeklySaveFields(report, reqVO, profile); + report.setStatusCode(getInitialStatusCode()); + weeklyReportMapper.insert(report); + replaceWeeklyChildren(report.getId(), reqVO); + WeeklyReportDO latest = weeklyReportMapper.selectById(report.getId()); + writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, latest.getId(), WorkReportConstants.ACTION_CREATE, + null, latest.getStatusCode(), null, buildWeeklyRemark(latest)); + return report.getId(); + } + + @Transactional(rollbackFor = Exception.class) + public void updateWeeklyReport(Long id, WeeklyReportSaveReqVO reqVO) { + validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + WeeklyReportDO current = validateWeeklyReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getReporterId()); + validateAllowEdit(current.getStatusCode()); + validateWeeklyDuplicate(loginUserId, reqVO.getPeriodKey(), id); + + CurrentUserProfile profile = loadCurrentUserProfile(false); + WeeklyReportDO update = new WeeklyReportDO(); + update.setId(id); + applyWeeklySaveFields(update, reqVO, profile); + int updateCount = weeklyReportMapper.updateByIdAndStatus(update, id, current.getStatusCode()); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + replaceWeeklyChildren(id, reqVO); + WeeklyReportDO latest = weeklyReportMapper.selectById(id); + writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, id, WorkReportConstants.ACTION_UPDATE, + current.getStatusCode(), latest.getStatusCode(), null, buildWeeklyRemark(latest)); + } + + public WeeklyReportRespVO getWeeklyReport(Long id) { + WeeklyReportDO report = validateReadableWeeklyReport(id); + return toWeeklyRespVO(report, true); + } + + public PageResult getWeeklyReportPage(WeeklyReportPageReqVO reqVO) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + PageResult pageResult = weeklyReportMapper.selectReporterPage(loginUserId, reqVO, + getEnabledStatusCodes()); + return new PageResult<>(pageResult.getList().stream() + .map(report -> toWeeklyRespVO(report, false)) + .collect(Collectors.toList()), pageResult.getTotal()); + } + + public PageResult getWeeklyApprovalPage(WeeklyReportPageReqVO reqVO) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + PageResult pageResult = weeklyReportMapper.selectApprovalPage(loginUserId, reqVO); + return new PageResult<>(pageResult.getList().stream() + .map(report -> toWeeklyRespVO(report, false)) + .collect(Collectors.toList()), pageResult.getTotal()); + } + + @Transactional(rollbackFor = Exception.class) + public void submitWeeklyReport(Long id) { + WeeklyReportDO current = validateWeeklyReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getReporterId()); + String actionCode = resolveSubmitAction(current.getStatusCode()); + ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, null); + CurrentUserProfile profile = loadCurrentUserProfile(true); + + LocalDateTime submitTime = LocalDateTime.now(); + int updateCount = weeklyReportMapper.updateSubmitFieldsByIdAndStatus(id, current.getStatusCode(), + profile.deptName(), profile.postName(), profile.directManagerId(), profile.directManagerName(), + transition.getToStatusCode(), submitTime, String.valueOf(loginUserId)); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + + WeeklyReportDO latest = weeklyReportMapper.selectById(id); + writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_WEEKLY, actionCode, current.getStatusCode(), + transition.getToStatusCode(), null, buildWeeklyRemark(latest)); + writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, id, actionCode, current.getStatusCode(), + transition.getToStatusCode(), null, buildWeeklyRemark(latest)); + } + + @Transactional(rollbackFor = Exception.class) + public void approveWeeklyReport(Long id, WorkReportStatusActionReqVO reqVO) { + processWeeklyApproval(id, WorkReportConstants.ACTION_APPROVE, normalizeReason(reqVO)); + } + + @Transactional(rollbackFor = Exception.class) + public void rejectWeeklyReport(Long id, WorkReportStatusActionReqVO reqVO) { + processWeeklyApproval(id, WorkReportConstants.ACTION_REJECT, normalizeReason(reqVO)); + } + + @Transactional(rollbackFor = Exception.class) + public void deleteWeeklyReport(Long id) { + WeeklyReportDO current = validateWeeklyReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getReporterId()); + validateDeleteAllowed(current.getStatusCode()); + int deleteCount = weeklyReportMapper.deleteByIdAndStatusesAndReporterId(id, ALLOW_DELETE_STATUSES, loginUserId); + if (deleteCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_DELETE_NOT_ALLOWED); + } + personalReportReviewItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, id); + personalReportPlanItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, id); + weeklyReportTravelSegmentMapper.deleteByWeeklyReportId(id); + weeklyReportApprovalRecordMapper.deleteByWeeklyReportId(id); + workReportStatusLogMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, id); + writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, id, WorkReportConstants.ACTION_DELETE, + current.getStatusCode(), null, null, buildWeeklyRemark(current)); + } + + public List getWeeklyStatusLogs(Long id) { + validateReadableWeeklyReport(id); + return BeanUtils.toBean(workReportStatusLogMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, id), + WorkReportStatusLogRespVO.class); + } + + public List getWeeklyApprovalRecords(Long id) { + validateReadableWeeklyReport(id); + return BeanUtils.toBean(weeklyReportApprovalRecordMapper.selectListByWeeklyReportId(id), + WorkReportApprovalRecordRespVO.class); + } + + public List getWeeklyExportList(WeeklyReportPageReqVO reqVO) { + reqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + return BeanUtils.toBean(getWeeklyReportPage(reqVO).getList(), WeeklyReportExportVO.class); + } + + public MonthlyReportRespVO initMonthlyReport() { + CurrentUserProfile profile = loadCurrentUserProfile(true); + MonthlyReportRespVO respVO = new MonthlyReportRespVO(); + fillPersonalBase(respVO, profile); + applyStatusView(respVO, getInitialStatusModel()); + respVO.setReviewItems(Collections.emptyList()); + respVO.setPlanItems(Collections.emptyList()); + return respVO; + } + + @Transactional(rollbackFor = Exception.class) + public Long createMonthlyReport(MonthlyReportSaveReqVO reqVO) { + validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + CurrentUserProfile profile = loadCurrentUserProfile(true); + validateMonthlyDuplicate(profile.userId(), reqVO.getPeriodKey(), null); + + MonthlyReportDO report = new MonthlyReportDO(); + applyMonthlySaveFields(report, reqVO, profile); + report.setStatusCode(getInitialStatusCode()); + monthlyReportMapper.insert(report); + replaceMonthlyChildren(report.getId(), reqVO); + MonthlyReportDO latest = monthlyReportMapper.selectById(report.getId()); + writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, latest.getId(), WorkReportConstants.ACTION_CREATE, + null, latest.getStatusCode(), null, buildMonthlyRemark(latest)); + return report.getId(); + } + + @Transactional(rollbackFor = Exception.class) + public void updateMonthlyReport(Long id, MonthlyReportSaveReqVO reqVO) { + validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + MonthlyReportDO current = validateMonthlyReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getReporterId()); + validateAllowEdit(current.getStatusCode()); + validateMonthlyDuplicate(loginUserId, reqVO.getPeriodKey(), id); + + CurrentUserProfile profile = loadCurrentUserProfile(false); + MonthlyReportDO update = new MonthlyReportDO(); + update.setId(id); + applyMonthlySaveFields(update, reqVO, profile); + int updateCount = monthlyReportMapper.updateByIdAndStatus(update, id, current.getStatusCode()); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + replaceMonthlyChildren(id, reqVO); + MonthlyReportDO latest = monthlyReportMapper.selectById(id); + writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, id, WorkReportConstants.ACTION_UPDATE, + current.getStatusCode(), latest.getStatusCode(), null, buildMonthlyRemark(latest)); + } + + public MonthlyReportRespVO getMonthlyReport(Long id) { + MonthlyReportDO report = validateReadableMonthlyReport(id); + return toMonthlyRespVO(report, true); + } + + public PageResult getMonthlyReportPage(MonthlyReportPageReqVO reqVO) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + PageResult pageResult = monthlyReportMapper.selectReporterPage(loginUserId, reqVO, + getEnabledStatusCodes()); + return new PageResult<>(pageResult.getList().stream() + .map(report -> toMonthlyRespVO(report, false)) + .collect(Collectors.toList()), pageResult.getTotal()); + } + + public PageResult getMonthlyApprovalPage(MonthlyReportPageReqVO reqVO) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + PageResult pageResult = monthlyReportMapper.selectApprovalPage(loginUserId, reqVO); + return new PageResult<>(pageResult.getList().stream() + .map(report -> toMonthlyRespVO(report, false)) + .collect(Collectors.toList()), pageResult.getTotal()); + } + + @Transactional(rollbackFor = Exception.class) + public void submitMonthlyReport(Long id) { + MonthlyReportDO current = validateMonthlyReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getReporterId()); + String actionCode = resolveSubmitAction(current.getStatusCode()); + ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, null); + CurrentUserProfile profile = loadCurrentUserProfile(true); + + LocalDateTime submitTime = LocalDateTime.now(); + int updateCount = monthlyReportMapper.updateSubmitFieldsByIdAndStatus(id, current.getStatusCode(), + profile.deptName(), profile.postName(), profile.directManagerId(), profile.directManagerName(), + transition.getToStatusCode(), submitTime, String.valueOf(loginUserId)); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + + MonthlyReportDO latest = monthlyReportMapper.selectById(id); + writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_MONTHLY, actionCode, current.getStatusCode(), + transition.getToStatusCode(), null, buildMonthlyRemark(latest)); + writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, id, actionCode, current.getStatusCode(), + transition.getToStatusCode(), null, buildMonthlyRemark(latest)); + } + + @Transactional(rollbackFor = Exception.class) + public void approveMonthlyReport(Long id, MonthlyReportApproveReqVO reqVO) { + processMonthlyApproval(id, WorkReportConstants.ACTION_APPROVE, reqVO); + } + + @Transactional(rollbackFor = Exception.class) + public void rejectMonthlyReport(Long id, WorkReportStatusActionReqVO reqVO) { + MonthlyReportApproveReqVO rejectReqVO = new MonthlyReportApproveReqVO(); + rejectReqVO.setReason(normalizeReason(reqVO)); + processMonthlyApproval(id, WorkReportConstants.ACTION_REJECT, rejectReqVO); + } + + @Transactional(rollbackFor = Exception.class) + public void deleteMonthlyReport(Long id) { + MonthlyReportDO current = validateMonthlyReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getReporterId()); + validateDeleteAllowed(current.getStatusCode()); + int deleteCount = monthlyReportMapper.deleteByIdAndStatusesAndReporterId(id, ALLOW_DELETE_STATUSES, loginUserId); + if (deleteCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_DELETE_NOT_ALLOWED); + } + personalReportReviewItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, id); + personalReportPlanItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, id); + monthlyReportApprovalRecordMapper.deleteByMonthlyReportId(id); + workReportStatusLogMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, id); + writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, id, WorkReportConstants.ACTION_DELETE, + current.getStatusCode(), null, null, buildMonthlyRemark(current)); + } + + public List getMonthlyStatusLogs(Long id) { + validateReadableMonthlyReport(id); + return BeanUtils.toBean(workReportStatusLogMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, id), + WorkReportStatusLogRespVO.class); + } + + public List getMonthlyApprovalRecords(Long id) { + validateReadableMonthlyReport(id); + return BeanUtils.toBean(monthlyReportApprovalRecordMapper.selectListByMonthlyReportId(id), + MonthlyReportApprovalRecordRespVO.class); + } + + public List getMonthlyExportList(MonthlyReportPageReqVO reqVO) { + reqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + return BeanUtils.toBean(getMonthlyReportPage(reqVO).getList(), MonthlyReportExportVO.class); + } + + public ProjectReportRespVO initProjectReport(Long projectId) { + CurrentUserProfile profile = loadCurrentUserProfile(true); + ProjectDO project = validateProjectExists(projectId); + ProjectReportRespVO respVO = new ProjectReportRespVO(); + respVO.setProjectId(project.getId()); + respVO.setProjectName(project.getProjectName()); + respVO.setProjectOwnerId(profile.userId()); + respVO.setProjectOwnerName(profile.userName()); + respVO.setProjectMemberSnapshot(BeanUtils.toBean(buildProjectMemberSnapshot(projectId), + WorkReportMemberSnapshotRespVO.class)); + respVO.setSupervisorUserId(profile.directManagerId()); + respVO.setSupervisorName(profile.directManagerName()); + applyStatusView(respVO, getInitialStatusModel()); + respVO.setCurrentItems(Collections.emptyList()); + respVO.setNextItems(Collections.emptyList()); + return respVO; + } + + @Transactional(rollbackFor = Exception.class) + public Long createProjectReport(ProjectReportSaveReqVO reqVO) { + validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + validateProjectFlag(reqVO.getFlag()); + CurrentUserProfile profile = loadCurrentUserProfile(true); + validateProjectReportDuplicate(reqVO.getProjectId(), reqVO.getPeriodKey(), profile.userId(), null); + ProjectDO project = validateProjectExists(reqVO.getProjectId()); + + ProjectReportDO report = new ProjectReportDO(); + applyProjectSaveFields(report, reqVO, profile, project); + report.setStatusCode(getInitialStatusCode()); + projectReportMapper.insert(report); + replaceProjectChildren(report.getId(), reqVO); + ProjectReportDO latest = projectReportMapper.selectById(report.getId()); + writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, latest.getId(), WorkReportConstants.ACTION_CREATE, + null, latest.getStatusCode(), null, buildProjectRemark(latest)); + return report.getId(); + } + + @Transactional(rollbackFor = Exception.class) + public void updateProjectReport(Long id, ProjectReportSaveReqVO reqVO) { + validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + validateProjectFlag(reqVO.getFlag()); + ProjectReportDO current = validateProjectReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getProjectOwnerId()); + validateAllowEdit(current.getStatusCode()); + validateProjectReportDuplicate(reqVO.getProjectId(), reqVO.getPeriodKey(), loginUserId, id); + + CurrentUserProfile profile = loadCurrentUserProfile(false); + ProjectDO project = validateProjectExists(reqVO.getProjectId()); + ProjectReportDO update = new ProjectReportDO(); + update.setId(id); + applyProjectSaveFields(update, reqVO, profile, project); + update.setTechnicalOwnerName(current.getTechnicalOwnerName()); + int updateCount = projectReportMapper.updateByIdAndStatus(update, id, current.getStatusCode()); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + replaceProjectChildren(id, reqVO); + ProjectReportDO latest = projectReportMapper.selectById(id); + writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, id, WorkReportConstants.ACTION_UPDATE, + current.getStatusCode(), latest.getStatusCode(), null, buildProjectRemark(latest)); + } + + public ProjectReportRespVO getProjectReport(Long id) { + ProjectReportDO report = validateReadableProjectReport(id); + return toProjectRespVO(report, true); + } + + public PageResult getProjectReportPage(ProjectReportPageReqVO reqVO) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + PageResult pageResult = projectReportMapper.selectReporterPage(loginUserId, reqVO, + getEnabledStatusCodes()); + return new PageResult<>(pageResult.getList().stream() + .map(report -> toProjectRespVO(report, false)) + .collect(Collectors.toList()), pageResult.getTotal()); + } + + public PageResult getProjectApprovalPage(ProjectReportPageReqVO reqVO) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + PageResult pageResult = projectReportMapper.selectApprovalPage(loginUserId, reqVO); + return new PageResult<>(pageResult.getList().stream() + .map(report -> toProjectRespVO(report, false)) + .collect(Collectors.toList()), pageResult.getTotal()); + } + + @Transactional(rollbackFor = Exception.class) + public void submitProjectReport(Long id) { + ProjectReportDO current = validateProjectReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getProjectOwnerId()); + String actionCode = resolveSubmitAction(current.getStatusCode()); + ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, null); + CurrentUserProfile profile = loadCurrentUserProfile(true); + ProjectDO project = validateProjectExists(current.getProjectId()); + + LocalDateTime submitTime = LocalDateTime.now(); + int updateCount = projectReportMapper.updateSubmitFieldsByIdAndStatus(id, current.getStatusCode(), + project.getProjectName(), profile.userId(), profile.userName(), buildProjectMemberSnapshot(project.getId()), + profile.directManagerId(), profile.directManagerName(), transition.getToStatusCode(), submitTime, + String.valueOf(loginUserId)); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + + ProjectReportDO latest = projectReportMapper.selectById(id); + writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_PROJECT, actionCode, current.getStatusCode(), + transition.getToStatusCode(), null, buildProjectRemark(latest)); + writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, id, actionCode, current.getStatusCode(), + transition.getToStatusCode(), null, buildProjectRemark(latest)); + } + + @Transactional(rollbackFor = Exception.class) + public void approveProjectReport(Long id, WorkReportStatusActionReqVO reqVO) { + processProjectApproval(id, WorkReportConstants.ACTION_APPROVE, normalizeReason(reqVO)); + } + + @Transactional(rollbackFor = Exception.class) + public void rejectProjectReport(Long id, WorkReportStatusActionReqVO reqVO) { + processProjectApproval(id, WorkReportConstants.ACTION_REJECT, normalizeReason(reqVO)); + } + + @Transactional(rollbackFor = Exception.class) + public void deleteProjectReport(Long id) { + ProjectReportDO current = validateProjectReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getProjectOwnerId()); + validateDeleteAllowed(current.getStatusCode()); + int deleteCount = projectReportMapper.deleteByIdAndStatusesAndReporterId(id, ALLOW_DELETE_STATUSES, loginUserId); + if (deleteCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_DELETE_NOT_ALLOWED); + } + projectReportCurrentItemMapper.deleteByReportId(id); + projectReportNextItemMapper.deleteByReportId(id); + projectReportApprovalRecordMapper.deleteByProjectReportId(id); + workReportStatusLogMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_PROJECT, id); + writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, id, WorkReportConstants.ACTION_DELETE, + current.getStatusCode(), null, null, buildProjectRemark(current)); + } + + public List getProjectStatusLogs(Long id) { + validateReadableProjectReport(id); + return BeanUtils.toBean(workReportStatusLogMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_PROJECT, id), + WorkReportStatusLogRespVO.class); + } + + public List getProjectApprovalRecords(Long id) { + validateReadableProjectReport(id); + return BeanUtils.toBean(projectReportApprovalRecordMapper.selectListByProjectReportId(id), + WorkReportApprovalRecordRespVO.class); + } + + public List getProjectExportList(ProjectReportPageReqVO reqVO) { + reqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + return BeanUtils.toBean(getProjectReportPage(reqVO).getList(), ProjectReportExportVO.class); + } + + private void processWeeklyApproval(Long id, String actionCode, String reason) { + WeeklyReportDO current = validateWeeklyReportExists(id); + Long approverUserId = SecurityFrameworkUtils.getLoginUserId(); + validateApprover(approverUserId, current.getSupervisorUserId()); + ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, reason); + + LocalDateTime approvalTime = LocalDateTime.now(); + int updateCount = weeklyReportMapper.updateApprovalFieldsByIdAndStatus(id, current.getStatusCode(), + transition.getToStatusCode(), approvalTime, reason, reason, String.valueOf(approverUserId)); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + + WeeklyReportDO latest = weeklyReportMapper.selectById(id); + WorkReportStatusLogDO statusLog = writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_WEEKLY, actionCode, + current.getStatusCode(), transition.getToStatusCode(), reason, buildWeeklyRemark(latest)); + writeWeeklyApprovalRecord(latest, statusLog, reason); + writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, id, actionCode, current.getStatusCode(), + transition.getToStatusCode(), reason, buildWeeklyRemark(latest)); + } + + private void processMonthlyApproval(Long id, String actionCode, MonthlyReportApproveReqVO reqVO) { + MonthlyReportDO current = validateMonthlyReportExists(id); + Long approverUserId = SecurityFrameworkUtils.getLoginUserId(); + validateApprover(approverUserId, current.getSupervisorUserId()); + String reason = normalizeReason(reqVO); + ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, reason); + + LocalDateTime approvalTime = LocalDateTime.now(); + int updateCount = monthlyReportMapper.updateApprovalFieldsByIdAndStatus(id, current.getStatusCode(), + transition.getToStatusCode(), approvalTime, reason, reason, String.valueOf(approverUserId)); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + + MonthlyReportDO latest = monthlyReportMapper.selectById(id); + WorkReportStatusLogDO statusLog = writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_MONTHLY, actionCode, + current.getStatusCode(), transition.getToStatusCode(), reason, buildMonthlyRemark(latest)); + writeMonthlyApprovalRecord(latest, statusLog, actionCode, reqVO); + writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, id, actionCode, current.getStatusCode(), + transition.getToStatusCode(), reason, buildMonthlyRemark(latest)); + } + + private void processProjectApproval(Long id, String actionCode, String reason) { + ProjectReportDO current = validateProjectReportExists(id); + Long approverUserId = SecurityFrameworkUtils.getLoginUserId(); + validateApprover(approverUserId, current.getSupervisorUserId()); + ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, reason); + + LocalDateTime approvalTime = LocalDateTime.now(); + int updateCount = projectReportMapper.updateApprovalFieldsByIdAndStatus(id, current.getStatusCode(), + transition.getToStatusCode(), approvalTime, reason, reason, String.valueOf(approverUserId)); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + + ProjectReportDO latest = projectReportMapper.selectById(id); + WorkReportStatusLogDO statusLog = writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_PROJECT, actionCode, + current.getStatusCode(), transition.getToStatusCode(), reason, buildProjectRemark(latest)); + writeProjectApprovalRecord(latest, statusLog, reason); + writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, id, actionCode, current.getStatusCode(), + transition.getToStatusCode(), reason, buildProjectRemark(latest)); + } + + private WeeklyReportDO validateWeeklyReportExists(Long id) { + WeeklyReportDO report = weeklyReportMapper.selectById(id); + if (report == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_NOT_EXISTS); + } + return report; + } + + private MonthlyReportDO validateMonthlyReportExists(Long id) { + MonthlyReportDO report = monthlyReportMapper.selectById(id); + if (report == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_NOT_EXISTS); + } + return report; + } + + private ProjectReportDO validateProjectReportExists(Long id) { + ProjectReportDO report = projectReportMapper.selectById(id); + if (report == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_NOT_EXISTS); + } + return report; + } + + private WeeklyReportDO validateReadableWeeklyReport(Long id) { + WeeklyReportDO report = validateWeeklyReportExists(id); + validateReadable(report.getReporterId(), report.getSupervisorUserId()); + return report; + } + + private MonthlyReportDO validateReadableMonthlyReport(Long id) { + MonthlyReportDO report = validateMonthlyReportExists(id); + validateReadable(report.getReporterId(), report.getSupervisorUserId()); + return report; + } + + private ProjectReportDO validateReadableProjectReport(Long id) { + ProjectReportDO report = validateProjectReportExists(id); + validateReadable(report.getProjectOwnerId(), report.getSupervisorUserId()); + return report; + } + + private void validateReadable(Long reporterId, Long supervisorUserId) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + if (!Objects.equals(loginUserId, reporterId) && !Objects.equals(loginUserId, supervisorUserId)) { + throw exception(ErrorCodeConstants.WORK_REPORT_READ_FORBIDDEN); + } + } + + private void validateReporter(Long loginUserId, Long reporterId) { + if (!Objects.equals(loginUserId, reporterId)) { + throw exception(ErrorCodeConstants.WORK_REPORT_REPORTER_ONLY); + } + } + + private void validateApprover(Long loginUserId, Long approverId) { + if (!Objects.equals(loginUserId, approverId)) { + throw exception(ErrorCodeConstants.WORK_REPORT_APPROVER_ONLY); + } + } + + private void validateDeleteAllowed(String statusCode) { + if (WorkReportConstants.STATUS_APPROVED.equals(statusCode)) { + throw exception(ErrorCodeConstants.WORK_REPORT_DELETE_NOT_ALLOWED); + } + } + + private void validateAllowEdit(String statusCode) { + ObjectStatusModelDO statusModel = getStatusModel(statusCode); + if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_NOT_ALLOW_EDIT); + } + } + + private ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) { + ObjectStatusTransitionDO transition = objectStatusTransitionMapper + .selectByObjectTypeAndFromStatusAndAction(WorkReportConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode); + if (transition == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_ACTION_NOT_ALLOWED, actionCode); + } + if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_ACTION_REASON_REQUIRED, actionCode); + } + getStatusModel(transition.getToStatusCode()); + return transition; + } + + private ObjectStatusModelDO getStatusModel(String statusCode) { + ObjectStatusModelDO statusModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled( + WorkReportConstants.STATUS_OBJECT_TYPE, statusCode); + if (statusModel == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED); + } + return statusModel; + } + + private List getEnabledStatusCodes() { + return objectStatusModelMapper.selectListByObjectTypeEnabled(WorkReportConstants.STATUS_OBJECT_TYPE).stream() + .map(ObjectStatusModelDO::getStatusCode) + .filter(StringUtils::hasText) + .collect(Collectors.toList()); + } + + private ObjectStatusModelDO getInitialStatusModel() { + ObjectStatusModelDO statusModel = objectStatusModelMapper + .selectInitialByObjectTypeEnabled(WorkReportConstants.STATUS_OBJECT_TYPE); + if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED); + } + return statusModel; + } + + private String getInitialStatusCode() { + return getInitialStatusModel().getStatusCode(); + } + + private String resolveSubmitAction(String statusCode) { + if (WorkReportConstants.STATUS_DRAFT.equals(statusCode)) { + return WorkReportConstants.ACTION_SUBMIT; + } + if (WorkReportConstants.STATUS_REJECTED.equals(statusCode)) { + return WorkReportConstants.ACTION_RESUBMIT; + } + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_ACTION_NOT_ALLOWED, WorkReportConstants.ACTION_SUBMIT); + } + + private void validateWeeklyDuplicate(Long reporterId, String periodKey, Long excludeId) { + WeeklyReportDO exist = weeklyReportMapper.selectByReporterIdAndPeriodKey(reporterId, normalizeRequiredText(periodKey, "周期编码不能为空")); + if (exist != null && !Objects.equals(exist.getId(), excludeId)) { + throw exception(ErrorCodeConstants.WORK_REPORT_PERIOD_DUPLICATE); + } + } + + private void validateMonthlyDuplicate(Long reporterId, String periodKey, Long excludeId) { + MonthlyReportDO exist = monthlyReportMapper.selectByReporterIdAndPeriodKey(reporterId, normalizeRequiredText(periodKey, "周期编码不能为空")); + if (exist != null && !Objects.equals(exist.getId(), excludeId)) { + throw exception(ErrorCodeConstants.WORK_REPORT_PERIOD_DUPLICATE); + } + } + + private void validateProjectReportDuplicate(Long projectId, String periodKey, Long reporterId, Long excludeId) { + ProjectReportDO exist = projectReportMapper.selectByProjectIdAndPeriodKeyAndProjectOwnerId(projectId, + normalizeRequiredText(periodKey, "周期编码不能为空"), reporterId); + if (exist != null && !Objects.equals(exist.getId(), excludeId)) { + throw exception(ErrorCodeConstants.WORK_REPORT_PERIOD_DUPLICATE); + } + } + + private void validatePeriod(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) { + throw invalidParamException("周期开始日期和结束日期不能为空"); + } + if (endDate.isBefore(startDate)) { + throw invalidParamException("周期结束日期不能早于开始日期"); + } + } + + private void validateProjectFlag(Integer flag) { + if (!Objects.equals(flag, 1) && !Objects.equals(flag, 2)) { + throw invalidParamException("上半月/下半月标记只能为 1 或 2"); + } + } + + private CurrentUserProfile loadCurrentUserProfile(boolean requireManager) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + AdminUserRespDTO user = loadUser(userId); + String userName = StringUtils.hasText(user.getNickname()) + ? user.getNickname().trim() + : defaultText(SecurityFrameworkUtils.getLoginUserNickname()); + + String deptName = null; + if (user.getDeptId() != null) { + CommonResult deptResult = deptApi.getDept(user.getDeptId()); + DeptRespDTO dept = deptResult == null ? null : deptResult.getCheckedData(); + deptName = dept == null ? null : dept.getName(); + } + + String postName = null; + if (user.getPositionId() != null) { + Map postMap = postApi.getPostMap(Collections.singleton(user.getPositionId())); + PostRespDTO post = postMap.get(user.getPositionId()); + postName = post == null ? null : post.getName(); + } + + Long directManagerId = null; + String directManagerName = null; + CommonResult directManagerResult = userManagementRelationApi.getDirectManager(userId); + AdminUserRespDTO directManager = directManagerResult == null ? null : directManagerResult.getCheckedData(); + if (directManager != null) { + directManagerId = directManager.getId(); + directManagerName = defaultText(directManager.getNickname()); + } + if (requireManager && directManagerId == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_DIRECT_MANAGER_NOT_EXISTS); + } + return new CurrentUserProfile(userId, defaultText(userName), deptName, postName, directManagerId, directManagerName); + } + + private AdminUserRespDTO loadUser(Long userId) { + if (userId == null) { + throw invalidParamException("用户编号不能为空"); + } + adminUserApi.validateUserList(Collections.singleton(userId)).getCheckedData(); + CommonResult result = adminUserApi.getUser(userId); + AdminUserRespDTO user = result == null ? null : result.getCheckedData(); + if (user == null) { + throw invalidParamException("用户不存在:{}", userId); + } + return user; + } + + private ProjectDO validateProjectExists(Long projectId) { + ProjectDO project = projectMapper.selectById(projectId); + if (project == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_PROJECT_NOT_EXISTS); + } + return project; + } + + private List buildProjectMemberSnapshot(Long projectId) { + List members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId); + Set userIds = new LinkedHashSet<>(); + for (UserObjectRoleDO member : members) { + if (member == null || !Objects.equals(member.getStatus(), 0) || member.getUserId() == null) { + continue; + } + userIds.add(member.getUserId()); + } + if (userIds.isEmpty()) { + return Collections.emptyList(); + } + Map userMap = adminUserApi.getUserMap(userIds); + List snapshot = new ArrayList<>(userIds.size()); + for (Long userId : userIds) { + WorkReportMemberSnapshotItem item = new WorkReportMemberSnapshotItem(); + item.setUserId(userId); + AdminUserRespDTO user = userMap.get(userId); + item.setUserName(user == null ? "" : defaultText(user.getNickname())); + snapshot.add(item); + } + return snapshot; + } + + private void applyWeeklySaveFields(WeeklyReportDO target, WeeklyReportSaveReqVO reqVO, CurrentUserProfile profile) { + List travelSegments = Boolean.TRUE.equals(reqVO.getIsBusinessTrip()) + ? defaultList(reqVO.getTravelSegments()) : Collections.emptyList(); + validateTravelSegments(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate(), travelSegments); + target.setReporterId(profile.userId()); + target.setReporterName(profile.userName()); + target.setReporterDeptName(profile.deptName()); + target.setReporterPostName(profile.postName()); + target.setSupervisorUserId(profile.directManagerId()); + target.setSupervisorName(profile.directManagerName()); + target.setPeriodKey(normalizeRequiredText(reqVO.getPeriodKey(), "周期编码不能为空")); + target.setPeriodLabel(normalizeRequiredText(reqVO.getPeriodLabel(), "周期名称不能为空")); + target.setPeriodStartDate(reqVO.getPeriodStartDate()); + target.setPeriodEndDate(reqVO.getPeriodEndDate()); + target.setIsBusinessTrip(Boolean.TRUE.equals(reqVO.getIsBusinessTrip())); + target.setTotalTravelDays(sumTravelDays(travelSegments)); + target.setTotalWorkHours(sumReviewWorkHours(reqVO.getReviewItems())); + } + + private void applyMonthlySaveFields(MonthlyReportDO target, MonthlyReportSaveReqVO reqVO, CurrentUserProfile profile) { + target.setReporterId(profile.userId()); + target.setReporterName(profile.userName()); + target.setReporterDeptName(profile.deptName()); + target.setReporterPostName(profile.postName()); + target.setSupervisorUserId(profile.directManagerId()); + target.setSupervisorName(profile.directManagerName()); + target.setPeriodKey(normalizeRequiredText(reqVO.getPeriodKey(), "周期编码不能为空")); + target.setPeriodLabel(normalizeRequiredText(reqVO.getPeriodLabel(), "周期名称不能为空")); + target.setPeriodStartDate(reqVO.getPeriodStartDate()); + target.setPeriodEndDate(reqVO.getPeriodEndDate()); + target.setTotalWorkHours(sumReviewWorkHours(reqVO.getReviewItems())); + } + + private void applyProjectSaveFields(ProjectReportDO target, ProjectReportSaveReqVO reqVO, + CurrentUserProfile profile, ProjectDO project) { + target.setProjectId(project.getId()); + target.setProjectName(project.getProjectName()); + target.setProjectOwnerId(profile.userId()); + target.setProjectOwnerName(profile.userName()); + target.setProjectMemberSnapshot(buildProjectMemberSnapshot(project.getId())); + target.setSupervisorUserId(profile.directManagerId()); + target.setSupervisorName(profile.directManagerName()); + target.setPeriodKey(normalizeRequiredText(reqVO.getPeriodKey(), "周期编码不能为空")); + target.setPeriodLabel(normalizeRequiredText(reqVO.getPeriodLabel(), "周期名称不能为空")); + target.setPeriodStartDate(reqVO.getPeriodStartDate()); + target.setPeriodEndDate(reqVO.getPeriodEndDate()); + target.setFlag(reqVO.getFlag()); + target.setProjectStatusDesc(normalizeNullableText(reqVO.getProjectStatusDesc())); + target.setProjectProgressPlan(normalizeNullableText(reqVO.getProjectProgressPlan())); + target.setProjectKeyPoints(normalizeNullableText(reqVO.getProjectKeyPoints())); + target.setProjectProblems(normalizeNullableText(reqVO.getProjectProblems())); + target.setTotalWorkHours(sumProjectCurrentWorkHours(reqVO.getCurrentItems())); + } + + private void replaceWeeklyChildren(Long reportId, WeeklyReportSaveReqVO reqVO) { + personalReportReviewItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, reportId); + personalReportPlanItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, reportId); + weeklyReportTravelSegmentMapper.deleteByWeeklyReportId(reportId); + insertReviewItems(WorkReportConstants.REPORT_TYPE_WEEKLY, reportId, reqVO.getReviewItems()); + insertPlanItems(WorkReportConstants.REPORT_TYPE_WEEKLY, reportId, reqVO.getPlanItems()); + if (Boolean.TRUE.equals(reqVO.getIsBusinessTrip())) { + insertTravelSegments(reportId, reqVO.getTravelSegments()); + } + } + + private void replaceMonthlyChildren(Long reportId, MonthlyReportSaveReqVO reqVO) { + personalReportReviewItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, reportId); + personalReportPlanItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, reportId); + insertReviewItems(WorkReportConstants.REPORT_TYPE_MONTHLY, reportId, reqVO.getReviewItems()); + insertPlanItems(WorkReportConstants.REPORT_TYPE_MONTHLY, reportId, reqVO.getPlanItems()); + } + + private void replaceProjectChildren(Long reportId, ProjectReportSaveReqVO reqVO) { + projectReportCurrentItemMapper.deleteByReportId(reportId); + projectReportNextItemMapper.deleteByReportId(reportId); + insertProjectCurrentItems(reportId, reqVO.getCurrentItems()); + insertProjectNextItems(reportId, reqVO.getNextItems()); + } + + private void insertReviewItems(String reportType, Long reportId, List items) { + List source = defaultList(items); + for (int i = 0; i < source.size(); i++) { + PersonalReportReviewItemReqVO item = source.get(i); + if (!StringUtils.hasText(item.getItemTitle())) { + continue; + } + PersonalReportReviewItemDO data = new PersonalReportReviewItemDO(); + data.setReportType(reportType); + data.setReportId(reportId); + data.setItemNumber(item.getItemNumber() != null ? item.getItemNumber() : i + 1); + data.setItemTitle(item.getItemTitle().trim()); + data.setWorkHours(item.getWorkHours()); + data.setContentText(normalizeNullableText(item.getContentText())); + data.setContentJson(item.getContentJson()); + data.setReflectionText(normalizeNullableText(item.getReflectionText())); + personalReportReviewItemMapper.insert(data); + } + } + + private void insertPlanItems(String reportType, Long reportId, List items) { + List source = defaultList(items); + for (int i = 0; i < source.size(); i++) { + PersonalReportPlanItemReqVO item = source.get(i); + if (!StringUtils.hasText(item.getItemTitle())) { + continue; + } + PersonalReportPlanItemDO data = new PersonalReportPlanItemDO(); + data.setReportType(reportType); + data.setReportId(reportId); + data.setItemNumber(item.getItemNumber() != null ? item.getItemNumber() : i + 1); + data.setItemTitle(item.getItemTitle().trim()); + data.setTargetText(normalizeNullableText(item.getTargetText())); + data.setTargetJson(item.getTargetJson()); + data.setSupportNeed(normalizeNullableText(item.getSupportNeed())); + personalReportPlanItemMapper.insert(data); + } + } + + private void insertTravelSegments(Long reportId, List items) { + List source = defaultList(items); + for (int i = 0; i < source.size(); i++) { + WeeklyReportTravelSegmentReqVO item = source.get(i); + if (item.getStartDate() == null || item.getEndDate() == null) { + continue; + } + WeeklyReportTravelSegmentDO data = new WeeklyReportTravelSegmentDO(); + data.setWeeklyReportId(reportId); + data.setSort(item.getSort() != null ? item.getSort() : i + 1); + data.setStartDate(item.getStartDate()); + data.setEndDate(item.getEndDate()); + data.setTravelDays(item.getTravelDays()); + data.setLocation(normalizeNullableText(item.getLocation())); + weeklyReportTravelSegmentMapper.insert(data); + } + } + + private void insertProjectCurrentItems(Long reportId, List items) { + List source = defaultList(items); + for (ProjectReportItemReqVO item : source) { + if (!StringUtils.hasText(item.getItemTitle())) { + continue; + } + ProjectReportCurrentItemDO data = new ProjectReportCurrentItemDO(); + data.setReportId(reportId); + data.setItemTitle(item.getItemTitle().trim()); + data.setWorkHours(item.getWorkHours()); + data.setPriorityCode(normalizeNullableText(item.getPriorityCode())); + data.setProgressRate(item.getProgressRate()); + projectReportCurrentItemMapper.insert(data); + } + } + + private void insertProjectNextItems(Long reportId, List items) { + List source = defaultList(items); + for (ProjectReportItemReqVO item : source) { + if (!StringUtils.hasText(item.getItemTitle())) { + continue; + } + ProjectReportNextItemDO data = new ProjectReportNextItemDO(); + data.setReportId(reportId); + data.setItemTitle(item.getItemTitle().trim()); + data.setPriorityCode(normalizeNullableText(item.getPriorityCode())); + data.setProgressRate(item.getProgressRate()); + projectReportNextItemMapper.insert(data); + } + } + + private WorkReportStatusLogDO writeStatusLog(Object report, String reportType, String actionType, + String fromStatus, String toStatus, String reason, String remark) { + WorkReportStatusLogDO log = new WorkReportStatusLogDO(); + log.setReportType(reportType); + log.setReportId(extractReportId(report)); + log.setActionType(actionType); + log.setFromStatus(fromStatus); + log.setToStatus(toStatus); + log.setReason(reason); + log.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId()); + log.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + log.setPeriodLabelSnapshot(extractPeriodLabel(report)); + log.setRemark(remark); + workReportStatusLogMapper.insert(log); + return log; + } + + private void writeAuditLog(String bizType, Long bizId, String actionType, String fromStatus, + String toStatus, String reason, String remark) { + BizAuditLogDO auditLog = new BizAuditLogDO(); + auditLog.setBizType(bizType); + auditLog.setBizId(bizId); + auditLog.setActionType(actionType); + auditLog.setFromStatus(fromStatus); + auditLog.setToStatus(toStatus); + auditLog.setReason(reason); + auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId()); + auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + auditLog.setRemark(remark); + bizAuditLogMapper.insert(auditLog); + } + + private void writeWeeklyApprovalRecord(WeeklyReportDO report, WorkReportStatusLogDO statusLog, String reason) { + WeeklyReportApprovalRecordDO record = new WeeklyReportApprovalRecordDO(); + record.setWeeklyReportId(report.getId()); + record.setStatusLogId(statusLog.getId()); + record.setApprovalRound(weeklyReportApprovalRecordMapper.countByWeeklyReportId(report.getId()) + 1); + record.setConclusion(statusLog.getToStatus()); + record.setOpinion(reason); + record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId()); + record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + weeklyReportApprovalRecordMapper.insert(record); + } + + private void writeMonthlyApprovalRecord(MonthlyReportDO report, WorkReportStatusLogDO statusLog, + String actionCode, MonthlyReportApproveReqVO reqVO) { + MonthlyReportApprovalRecordDO record = new MonthlyReportApprovalRecordDO(); + record.setMonthlyReportId(report.getId()); + record.setStatusLogId(statusLog.getId()); + record.setApprovalRound(monthlyReportApprovalRecordMapper.countByMonthlyReportId(report.getId()) + 1); + record.setConclusion(statusLog.getToStatus()); + record.setOpinion(normalizeReason(reqVO)); + if (WorkReportConstants.ACTION_APPROVE.equals(actionCode)) { + record.setMeetingDate(reqVO.getMeetingDate()); + record.setStrengthDesc(normalizeNullableText(reqVO.getStrengthDesc())); + record.setStrengthExample(normalizeNullableText(reqVO.getStrengthExample())); + record.setWeaknessDesc(normalizeNullableText(reqVO.getWeaknessDesc())); + record.setWeaknessExample(normalizeNullableText(reqVO.getWeaknessExample())); + record.setImprovementSuggestion(normalizeNullableText(reqVO.getImprovementSuggestion())); + record.setPerformanceResult(normalizeNullableText(reqVO.getPerformanceResult())); + record.setEmployeeSignName(normalizeNullableText(reqVO.getEmployeeSignName())); + record.setEmployeeSignedDate(reqVO.getEmployeeSignedDate()); + record.setSupervisorSignName(normalizeNullableText(reqVO.getSupervisorSignName())); + record.setSupervisorSignedDate(reqVO.getSupervisorSignedDate()); + } + record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId()); + record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + monthlyReportApprovalRecordMapper.insert(record); + } + + private void writeProjectApprovalRecord(ProjectReportDO report, WorkReportStatusLogDO statusLog, String reason) { + ProjectReportApprovalRecordDO record = new ProjectReportApprovalRecordDO(); + record.setProjectReportId(report.getId()); + record.setStatusLogId(statusLog.getId()); + record.setApprovalRound(projectReportApprovalRecordMapper.countByProjectReportId(report.getId()) + 1); + record.setConclusion(statusLog.getToStatus()); + record.setOpinion(reason); + record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId()); + record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + projectReportApprovalRecordMapper.insert(record); + } + + private Long extractReportId(Object report) { + if (report instanceof WeeklyReportDO weeklyReport) { + return weeklyReport.getId(); + } + if (report instanceof MonthlyReportDO monthlyReport) { + return monthlyReport.getId(); + } + if (report instanceof ProjectReportDO projectReport) { + return projectReport.getId(); + } + return null; + } + + private String extractPeriodLabel(Object report) { + if (report instanceof WeeklyReportDO weeklyReport) { + return weeklyReport.getPeriodLabel(); + } + if (report instanceof MonthlyReportDO monthlyReport) { + return monthlyReport.getPeriodLabel(); + } + if (report instanceof ProjectReportDO projectReport) { + return projectReport.getPeriodLabel(); + } + return null; + } + + private WeeklyReportRespVO toWeeklyRespVO(WeeklyReportDO report, boolean withChildren) { + WeeklyReportRespVO respVO = BeanUtils.toBean(report, WeeklyReportRespVO.class); + applyStatusView(respVO, getStatusModel(report.getStatusCode())); + if (withChildren) { + respVO.setReviewItems(BeanUtils.toBean( + personalReportReviewItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, report.getId()), + PersonalReportReviewItemRespVO.class)); + respVO.setPlanItems(BeanUtils.toBean( + personalReportPlanItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, report.getId()), + PersonalReportPlanItemRespVO.class)); + respVO.setTravelSegments(BeanUtils.toBean( + weeklyReportTravelSegmentMapper.selectListByWeeklyReportId(report.getId()), + WeeklyReportTravelSegmentRespVO.class)); + } + return respVO; + } + + private MonthlyReportRespVO toMonthlyRespVO(MonthlyReportDO report, boolean withChildren) { + MonthlyReportRespVO respVO = BeanUtils.toBean(report, MonthlyReportRespVO.class); + applyStatusView(respVO, getStatusModel(report.getStatusCode())); + if (withChildren) { + respVO.setReviewItems(BeanUtils.toBean( + personalReportReviewItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, report.getId()), + PersonalReportReviewItemRespVO.class)); + respVO.setPlanItems(BeanUtils.toBean( + personalReportPlanItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, report.getId()), + PersonalReportPlanItemRespVO.class)); + } + return respVO; + } + + private ProjectReportRespVO toProjectRespVO(ProjectReportDO report, boolean withChildren) { + ProjectReportRespVO respVO = BeanUtils.toBean(report, ProjectReportRespVO.class); + applyStatusView(respVO, getStatusModel(report.getStatusCode())); + respVO.setProjectMemberSnapshot(BeanUtils.toBean(defaultList(report.getProjectMemberSnapshot()), + WorkReportMemberSnapshotRespVO.class)); + if (withChildren) { + respVO.setCurrentItems(BeanUtils.toBean(projectReportCurrentItemMapper.selectListByReportId(report.getId()), + ProjectReportItemRespVO.class)); + respVO.setNextItems(BeanUtils.toBean(projectReportNextItemMapper.selectListByReportId(report.getId()), + ProjectReportItemRespVO.class)); + } + return respVO; + } + + private void fillPersonalBase(WeeklyReportRespVO respVO, CurrentUserProfile profile) { + respVO.setReporterId(profile.userId()); + respVO.setReporterName(profile.userName()); + respVO.setReporterDeptName(profile.deptName()); + respVO.setReporterPostName(profile.postName()); + respVO.setSupervisorUserId(profile.directManagerId()); + respVO.setSupervisorName(profile.directManagerName()); + } + + private void fillPersonalBase(MonthlyReportRespVO respVO, CurrentUserProfile profile) { + respVO.setReporterId(profile.userId()); + respVO.setReporterName(profile.userName()); + respVO.setReporterDeptName(profile.deptName()); + respVO.setReporterPostName(profile.postName()); + respVO.setSupervisorUserId(profile.directManagerId()); + respVO.setSupervisorName(profile.directManagerName()); + } + + private void applyStatusView(WeeklyReportRespVO respVO, ObjectStatusModelDO statusModel) { + respVO.setStatusCode(statusModel.getStatusCode()); + respVO.setStatusName(statusModel.getStatusName()); + respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit())); + respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag())); + } + + private void applyStatusView(MonthlyReportRespVO respVO, ObjectStatusModelDO statusModel) { + respVO.setStatusCode(statusModel.getStatusCode()); + respVO.setStatusName(statusModel.getStatusName()); + respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit())); + respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag())); + } + + private void applyStatusView(ProjectReportRespVO respVO, ObjectStatusModelDO statusModel) { + respVO.setStatusCode(statusModel.getStatusCode()); + respVO.setStatusName(statusModel.getStatusName()); + respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit())); + respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag())); + } + + private WorkReportStatusDictRespVO toStatusDictRespVO(ObjectStatusModelDO statusModel) { + WorkReportStatusDictRespVO respVO = new WorkReportStatusDictRespVO(); + respVO.setStatusCode(statusModel.getStatusCode()); + respVO.setStatusName(statusModel.getStatusName()); + respVO.setSort(statusModel.getSort()); + respVO.setInitialFlag(statusModel.getInitialFlag()); + respVO.setTerminalFlag(statusModel.getTerminalFlag()); + respVO.setAllowEdit(statusModel.getAllowEdit()); + return respVO; + } + + private void validateTravelSegments(LocalDate periodStartDate, LocalDate periodEndDate, + List travelSegments) { + for (WeeklyReportTravelSegmentReqVO item : defaultList(travelSegments)) { + if (item.getStartDate() == null || item.getEndDate() == null) { + continue; + } + if (item.getEndDate().isBefore(item.getStartDate())) { + throw invalidParamException("出差结束日期不能早于开始日期"); + } + if (item.getStartDate().isBefore(periodStartDate) || item.getEndDate().isAfter(periodEndDate)) { + throw invalidParamException("出差分段必须落在报告周期内"); + } + } + } + + private BigDecimal sumReviewWorkHours(List items) { + BigDecimal total = BigDecimal.ZERO; + for (PersonalReportReviewItemReqVO item : defaultList(items)) { + if (item.getWorkHours() != null) { + total = total.add(item.getWorkHours()); + } + } + return total.compareTo(BigDecimal.ZERO) == 0 ? null : total; + } + + private BigDecimal sumProjectCurrentWorkHours(List items) { + BigDecimal total = BigDecimal.ZERO; + for (ProjectReportItemReqVO item : defaultList(items)) { + if (item.getWorkHours() != null) { + total = total.add(item.getWorkHours()); + } + } + return total.compareTo(BigDecimal.ZERO) == 0 ? null : total; + } + + private BigDecimal sumTravelDays(List items) { + BigDecimal total = BigDecimal.ZERO; + for (WeeklyReportTravelSegmentReqVO item : defaultList(items)) { + if (item.getTravelDays() != null) { + total = total.add(item.getTravelDays()); + } + } + return total.compareTo(BigDecimal.ZERO) == 0 ? null : total; + } + + private String buildWeeklyRemark(WeeklyReportDO report) { + return "周报:" + defaultText(report.getReporterName()) + " / " + defaultText(report.getPeriodLabel()); + } + + private String buildMonthlyRemark(MonthlyReportDO report) { + return "月报:" + defaultText(report.getReporterName()) + " / " + defaultText(report.getPeriodLabel()); + } + + private String buildProjectRemark(ProjectReportDO report) { + return "项目半月报:" + defaultText(report.getProjectName()) + " / " + defaultText(report.getPeriodLabel()); + } + + private String normalizeReason(WorkReportStatusActionReqVO reqVO) { + return reqVO == null ? null : normalizeNullableText(reqVO.getReason()); + } + + private String normalizeRequiredText(String value, String message) { + if (!StringUtils.hasText(value)) { + throw invalidParamException(message); + } + return value.trim(); + } + + private String normalizeNullableText(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } + + private String defaultText(String value) { + return StringUtils.hasText(value) ? value.trim() : ""; + } + + private List defaultList(List list) { + return list == null ? Collections.emptyList() : list; + } + + private record CurrentUserProfile(Long userId, String userName, String deptName, String postName, + Long directManagerId, String directManagerName) { + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportStatusService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportStatusService.java new file mode 100644 index 0000000..7d085bf --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportStatusService.java @@ -0,0 +1,10 @@ +package com.njcn.rdms.module.project.service.workreport.common; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusDictRespVO; + +import java.util.List; + +public interface WorkReportStatusService { + + List getStatusDict(); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportStatusServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportStatusServiceImpl.java new file mode 100644 index 0000000..9c4f5d4 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportStatusServiceImpl.java @@ -0,0 +1,19 @@ +package com.njcn.rdms.module.project.service.workreport.common; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusDictRespVO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class WorkReportStatusServiceImpl implements WorkReportStatusService { + + @Resource + private WorkReportCommonService workReportCommonService; + + @Override + public List getStatusDict() { + return workReportCommonService.getStatusDict(); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/defaultdraft/WorkReportDefaultDraftService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/defaultdraft/WorkReportDefaultDraftService.java new file mode 100644 index 0000000..be9848e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/defaultdraft/WorkReportDefaultDraftService.java @@ -0,0 +1,930 @@ +package com.njcn.rdms.module.project.service.workreport.defaultdraft; + +import com.njcn.rdms.framework.common.biz.system.dict.dto.DictDataRespDTO; +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; +import com.njcn.rdms.module.project.constant.ProjectObjectConstants; +import com.njcn.rdms.module.project.constant.ProjectTaskConstants; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportMemberSnapshotRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportItemRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO; +import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO; +import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO; +import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO; +import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO; +import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskWorklogDO; +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportMemberSnapshotItem; +import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper; +import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper; +import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; +import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper; +import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper; +import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; +import com.njcn.rdms.module.project.enums.ErrorCodeConstants; +import com.njcn.rdms.module.system.api.dept.DeptApi; +import com.njcn.rdms.module.system.api.dept.PostApi; +import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO; +import com.njcn.rdms.module.system.api.dept.dto.PostRespDTO; +import com.njcn.rdms.module.system.api.dict.DictDataApi; +import com.njcn.rdms.module.system.api.user.AdminUserApi; +import com.njcn.rdms.module.system.api.user.UserManagementRelationApi; +import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; +import com.njcn.rdms.module.system.enums.DictTypeConstants; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +@Service +public class WorkReportDefaultDraftService { + + private static final String WORK_ITEM_MY_ITEMS = "我的事项"; + + @Resource + private ObjectStatusModelMapper objectStatusModelMapper; + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + @Resource + private PostApi postApi; + @Resource + private UserManagementRelationApi userManagementRelationApi; + @Resource + private DictDataApi dictDataApi; + @Resource + private ProjectMapper projectMapper; + @Resource + private UserObjectRoleMapper userObjectRoleMapper; + @Resource + private TaskWorklogMapper taskWorklogMapper; + @Resource + private ProjectTaskMapper projectTaskMapper; + @Resource + private PersonalItemMapper personalItemMapper; + @Resource + private ProjectExecutionMapper projectExecutionMapper; + + public WeeklyReportRespVO previewWeeklyDefaultDraft(WeeklyReportDefaultDraftReqVO reqVO) { + validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + CurrentUserProfile profile = loadCurrentUserProfile(true); + ObjectStatusModelDO initialStatus = getInitialStatusModel(); + + WeeklyReportRespVO respVO = new WeeklyReportRespVO(); + fillPersonalBase(respVO, profile); + respVO.setPeriodKey(reqVO.getPeriodKey()); + respVO.setPeriodLabel(reqVO.getPeriodLabel()); + respVO.setPeriodStartDate(reqVO.getPeriodStartDate()); + respVO.setPeriodEndDate(reqVO.getPeriodEndDate()); + respVO.setIsBusinessTrip(Boolean.FALSE); + respVO.setTravelSegments(Collections.emptyList()); + respVO.setTotalTravelDays(BigDecimal.ZERO); + applyStatusView(respVO, initialStatus); + + LocalDateRange nextRange = nextWeeklyRange(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + List reviewItems = buildPersonalReviewItems( + reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate(), profile.userId(), true); + List planItems = buildPersonalPlanItems( + nextRange.startDate(), nextRange.endDate(), profile.userId(), true); + respVO.setReviewItems(reviewItems); + respVO.setPlanItems(planItems); + respVO.setTotalWorkHours(sumReviewWorkHours(reviewItems)); + return respVO; + } + + public MonthlyReportRespVO previewMonthlyDefaultDraft(MonthlyReportDefaultDraftReqVO reqVO) { + validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + CurrentUserProfile profile = loadCurrentUserProfile(true); + ObjectStatusModelDO initialStatus = getInitialStatusModel(); + + MonthlyReportRespVO respVO = new MonthlyReportRespVO(); + fillPersonalBase(respVO, profile); + respVO.setPeriodKey(reqVO.getPeriodKey()); + respVO.setPeriodLabel(reqVO.getPeriodLabel()); + respVO.setPeriodStartDate(reqVO.getPeriodStartDate()); + respVO.setPeriodEndDate(reqVO.getPeriodEndDate()); + applyStatusView(respVO, initialStatus); + + LocalDateRange nextRange = nextMonthlyRange(reqVO.getPeriodEndDate()); + List reviewItems = buildPersonalReviewItems( + reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate(), profile.userId(), false); + List planItems = buildPersonalPlanItems( + nextRange.startDate(), nextRange.endDate(), profile.userId(), false); + respVO.setReviewItems(reviewItems); + respVO.setPlanItems(planItems); + respVO.setTotalWorkHours(sumReviewWorkHours(reviewItems)); + return respVO; + } + + public ProjectReportRespVO previewProjectDefaultDraft(Long projectId, ProjectReportDefaultDraftReqVO reqVO) { + validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + validateProjectFlag(reqVO.getFlag()); + CurrentUserProfile profile = loadCurrentUserProfile(true); + ProjectDO project = validateProjectExists(projectId); + ObjectStatusModelDO initialStatus = getInitialStatusModel(); + + ProjectReportRespVO respVO = new ProjectReportRespVO(); + respVO.setProjectId(project.getId()); + respVO.setProjectName(project.getProjectName()); + respVO.setProjectOwnerId(profile.userId()); + respVO.setProjectOwnerName(profile.userName()); + respVO.setTechnicalOwnerName(resolveProjectManagerName(project.getManagerUserId())); + respVO.setProjectMemberSnapshot(BeanUtils.toBean(buildProjectMemberSnapshot(projectId), + WorkReportMemberSnapshotRespVO.class)); + respVO.setSupervisorUserId(profile.directManagerId()); + respVO.setSupervisorName(profile.directManagerName()); + respVO.setPeriodKey(reqVO.getPeriodKey()); + respVO.setPeriodLabel(reqVO.getPeriodLabel()); + respVO.setPeriodStartDate(reqVO.getPeriodStartDate()); + respVO.setPeriodEndDate(reqVO.getPeriodEndDate()); + respVO.setFlag(reqVO.getFlag()); + applyStatusView(respVO, initialStatus); + + LocalDateRange nextRange = nextHalfMonthRange(reqVO.getPeriodEndDate()); + List currentItems = buildProjectCurrentItems( + projectId, reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); + List nextItems = buildProjectNextItems( + projectId, nextRange.startDate(), nextRange.endDate()); + respVO.setCurrentItems(currentItems); + respVO.setNextItems(nextItems); + respVO.setTotalWorkHours(sumProjectWorkHours(currentItems)); + return respVO; + } + + private List buildPersonalReviewItems(LocalDate periodStartDate, + LocalDate periodEndDate, + Long userId, + boolean weeklyMode) { + List worklogs = taskWorklogMapper.selectListByUserIdAndPeriod(userId, periodStartDate, periodEndDate); + if (worklogs.isEmpty()) { + return Collections.emptyList(); + } + + Set taskIds = worklogs.stream() + .map(TaskWorklogDO::getTaskId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (taskIds.isEmpty()) { + return Collections.emptyList(); + } + + Map taskMap = projectTaskMapper.selectBatchIds(taskIds).stream() + .collect(Collectors.toMap(ProjectTaskDO::getId, item -> item)); + Map itemMap = personalItemMapper.selectBatchIds(taskIds).stream() + .collect(Collectors.toMap(PersonalItemDO::getId, item -> item)); + + Set projectIds = taskMap.values().stream() + .map(ProjectTaskDO::getProjectId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map projectMap = projectIds.isEmpty() ? Collections.emptyMap() + : projectMapper.selectBatchIds(projectIds).stream() + .collect(Collectors.toMap(ProjectDO::getId, item -> item)); + Map taskItemTypeLabelMap = loadTaskItemTypeLabelMap(); + + Map aggregateMap = new LinkedHashMap<>(); + for (TaskWorklogDO worklog : worklogs) { + if (worklog.getTaskId() == null) { + continue; + } + ProjectTaskDO task = taskMap.get(worklog.getTaskId()); + if (task != null) { + ProjectDO project = projectMap.get(task.getProjectId()); + String workItemTitle = safeText(project == null ? null : project.getProjectName()); + String groupKey = workItemTitle + "||task||" + task.getId(); + ReviewAggregate aggregate = aggregateMap.computeIfAbsent(groupKey, key -> new ReviewAggregate( + workItemTitle, + safeText(task.getTaskTitle()), + safeText(task.getPriority()), + resolveTaskItemTypeLabel(task.getType(), taskItemTypeLabelMap))); + aggregate.merge(worklog); + continue; + } + PersonalItemDO item = itemMap.get(worklog.getTaskId()); + if (item != null) { + String groupKey = WORK_ITEM_MY_ITEMS + "||item||" + item.getId(); + ReviewAggregate aggregate = aggregateMap.computeIfAbsent(groupKey, key -> new ReviewAggregate( + WORK_ITEM_MY_ITEMS, + safeText(item.getTaskTitle()), + null, + resolveTaskItemTypeLabel(item.getType(), taskItemTypeLabelMap))); + aggregate.merge(worklog); + } + } + + List result = new ArrayList<>(); + int itemNumber = 1; + for (ReviewAggregate aggregate : aggregateMap.values()) { + if (isZeroProgress(aggregate.latestProgressRate())) { + continue; + } + PersonalReportReviewItemRespVO item = new PersonalReportReviewItemRespVO(); + item.setItemNumber(itemNumber++); + item.setItemTitle(aggregate.workItemTitle()); + item.setWorkHours(nullToZero(aggregate.totalWorkHours())); + item.setContentText(weeklyMode ? aggregate.buildWeeklyReviewText() : aggregate.buildMonthlyReviewText()); + item.setReflectionText(null); + result.add(item); + } + return result; + } + + private List buildPersonalPlanItems(LocalDate nextStartDate, + LocalDate nextEndDate, + Long userId, + boolean weeklyMode) { + List nextPeriodWorklogs = taskWorklogMapper.selectListByUserIdAndPeriod(userId, nextStartDate, nextEndDate); + Map worklogAggregateMap = aggregateTaskWorklogs(nextPeriodWorklogs); + + Map taskMap = loadPlanTaskMap(userId, nextStartDate, nextEndDate, worklogAggregateMap.keySet()); + Map itemMap = loadPlanItemMap(userId, nextStartDate, nextEndDate, worklogAggregateMap.keySet()); + + Set projectIds = taskMap.values().stream() + .map(ProjectTaskDO::getProjectId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map projectMap = projectIds.isEmpty() ? Collections.emptyMap() + : projectMapper.selectBatchIds(projectIds).stream() + .collect(Collectors.toMap(ProjectDO::getId, item -> item)); + Map taskItemTypeLabelMap = loadTaskItemTypeLabelMap(); + + Map aggregateMap = new LinkedHashMap<>(); + for (ProjectTaskDO task : taskMap.values()) { + TaskWorklogAggregate worklogAggregate = worklogAggregateMap.get(task.getId()); + if (!shouldIncludeTaskInPlan(task, nextStartDate, nextEndDate, worklogAggregate)) { + continue; + } + ProjectDO project = projectMap.get(task.getProjectId()); + String workItemTitle = safeText(project == null ? null : project.getProjectName()); + String categoryCode = safeText(task.getType()); + String categoryLabel = resolveTaskItemTypeLabel(task.getType(), taskItemTypeLabelMap); + String groupKey = workItemTitle + "||" + categoryCode; + PlanAggregate aggregate = aggregateMap.computeIfAbsent(groupKey, + key -> new PlanAggregate(workItemTitle, categoryLabel)); + aggregate.addTaskLine(safeText(task.getTaskTitle()), safeText(task.getPriority()), task.getProgressRate(), + worklogAggregate, weeklyMode); + } + for (PersonalItemDO item : itemMap.values()) { + TaskWorklogAggregate worklogAggregate = worklogAggregateMap.get(item.getId()); + if (!shouldIncludePersonalItemInPlan(item, nextStartDate, nextEndDate, worklogAggregate)) { + continue; + } + String categoryCode = safeText(item.getType()); + String categoryLabel = resolveTaskItemTypeLabel(item.getType(), taskItemTypeLabelMap); + String groupKey = WORK_ITEM_MY_ITEMS + "||" + categoryCode; + PlanAggregate aggregate = aggregateMap.computeIfAbsent(groupKey, + key -> new PlanAggregate(WORK_ITEM_MY_ITEMS, categoryLabel)); + aggregate.addItemLine(safeText(item.getTaskTitle()), item.getProgressRate(), worklogAggregate, weeklyMode); + } + + List result = new ArrayList<>(); + int itemNumber = 1; + for (PlanAggregate aggregate : aggregateMap.values()) { + if (aggregate.lines().isEmpty()) { + continue; + } + PersonalReportPlanItemRespVO item = new PersonalReportPlanItemRespVO(); + item.setItemNumber(itemNumber++); + item.setItemTitle(aggregate.workItemTitle()); + item.setTargetText(aggregate.buildTargetText()); + item.setSupportNeed(null); + result.add(item); + } + return result; + } + + private List buildProjectCurrentItems(Long projectId, + LocalDate periodStartDate, + LocalDate periodEndDate) { + List tasks = projectTaskMapper.selectListByProjectId(projectId); + if (tasks.isEmpty()) { + return Collections.emptyList(); + } + + Set taskIds = tasks.stream() + .map(ProjectTaskDO::getId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map taskMap = tasks.stream() + .collect(Collectors.toMap(ProjectTaskDO::getId, item -> item)); + List worklogs = taskWorklogMapper.selectListByTaskIdsAndPeriod(taskIds, periodStartDate, periodEndDate); + if (worklogs.isEmpty()) { + return Collections.emptyList(); + } + + Map executionMap = projectExecutionMapper.selectListByProjectId(projectId).stream() + .collect(Collectors.toMap(ProjectExecutionDO::getId, item -> item)); + Map hoursByExecutionId = new LinkedHashMap<>(); + for (TaskWorklogDO worklog : worklogs) { + ProjectTaskDO task = taskMap.get(worklog.getTaskId()); + if (task == null || task.getExecutionId() == null) { + continue; + } + hoursByExecutionId.merge(task.getExecutionId(), nullToZero(worklog.getDurationHours()), BigDecimal::add); + } + if (hoursByExecutionId.isEmpty()) { + return Collections.emptyList(); + } + + List progressExcludedStatusCodes = loadProgressExcludedTaskStatusCodes(); + List result = new ArrayList<>(); + for (Map.Entry entry : hoursByExecutionId.entrySet()) { + ProjectExecutionDO execution = executionMap.get(entry.getKey()); + if (execution == null) { + continue; + } + ProjectReportItemRespVO item = new ProjectReportItemRespVO(); + item.setItemTitle(safeText(execution.getExecutionName())); + item.setWorkHours(entry.getValue()); + item.setPriorityCode(safeText(execution.getPriority())); + item.setProgressRate(normalizeProgress(projectTaskMapper.selectRootTaskAvgProgressByExecutionId( + projectId, execution.getId(), progressExcludedStatusCodes))); + result.add(item); + } + result.sort(Comparator.comparing(ProjectReportItemRespVO::getItemTitle, Comparator.nullsLast(String::compareTo))); + return result; + } + + private List buildProjectNextItems(Long projectId, + LocalDate nextStartDate, + LocalDate nextEndDate) { + List plannedExecutions = projectExecutionMapper.selectPlannedListByProjectIdAndOverlap( + projectId, nextStartDate, nextEndDate, ProjectObjectConstants.STATUS_CANCELLED); + List zeroProgressExecutions = projectExecutionMapper.selectListByProjectIdAndStatusNot( + projectId, ProjectObjectConstants.STATUS_CANCELLED).stream() + .filter(execution -> isZeroProgress(execution.getProgressRate())) + .collect(Collectors.toList()); + Map executionMap = new LinkedHashMap<>(); + plannedExecutions.forEach(item -> executionMap.put(item.getId(), item)); + zeroProgressExecutions.forEach(item -> executionMap.put(item.getId(), item)); + if (executionMap.isEmpty()) { + return Collections.emptyList(); + } + + List progressExcludedStatusCodes = loadProgressExcludedTaskStatusCodes(); + List result = new ArrayList<>(); + for (ProjectExecutionDO execution : executionMap.values()) { + BigDecimal progress = normalizeProgress(projectTaskMapper.selectRootTaskAvgProgressByExecutionId( + projectId, execution.getId(), progressExcludedStatusCodes)); + if (isCompleted(progress)) { + continue; + } + ProjectReportItemRespVO item = new ProjectReportItemRespVO(); + item.setItemTitle(safeText(execution.getExecutionName())); + item.setWorkHours(BigDecimal.ZERO); + item.setPriorityCode(safeText(execution.getPriority())); + item.setProgressRate(progress); + result.add(item); + } + result.sort(Comparator.comparing(ProjectReportItemRespVO::getItemTitle, Comparator.nullsLast(String::compareTo))); + return result; + } + + private void validateProjectFlag(Integer flag) { + if (!Objects.equals(flag, 1) && !Objects.equals(flag, 2)) { + throw invalidParamException("上半月/下半月标记只能为 1 或 2"); + } + } + + private void validatePeriod(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) { + throw invalidParamException("周期开始日期和结束日期不能为空"); + } + if (endDate.isBefore(startDate)) { + throw invalidParamException("周期结束日期不能早于开始日期"); + } + } + + private ProjectDO validateProjectExists(Long projectId) { + ProjectDO project = projectMapper.selectById(projectId); + if (project == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_PROJECT_NOT_EXISTS); + } + return project; + } + + private ObjectStatusModelDO getInitialStatusModel() { + ObjectStatusModelDO statusModel = objectStatusModelMapper + .selectInitialByObjectTypeEnabled(WorkReportConstants.STATUS_OBJECT_TYPE); + if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED); + } + return statusModel; + } + + private void applyStatusView(WeeklyReportRespVO respVO, ObjectStatusModelDO statusModel) { + respVO.setStatusCode(statusModel.getStatusCode()); + respVO.setStatusName(statusModel.getStatusName()); + respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit())); + respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag())); + } + + private void applyStatusView(MonthlyReportRespVO respVO, ObjectStatusModelDO statusModel) { + respVO.setStatusCode(statusModel.getStatusCode()); + respVO.setStatusName(statusModel.getStatusName()); + respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit())); + respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag())); + } + + private void applyStatusView(ProjectReportRespVO respVO, ObjectStatusModelDO statusModel) { + respVO.setStatusCode(statusModel.getStatusCode()); + respVO.setStatusName(statusModel.getStatusName()); + respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit())); + respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag())); + } + + private void fillPersonalBase(WeeklyReportRespVO respVO, CurrentUserProfile profile) { + respVO.setReporterId(profile.userId()); + respVO.setReporterName(profile.userName()); + respVO.setReporterDeptName(profile.deptName()); + respVO.setReporterPostName(profile.postName()); + respVO.setSupervisorUserId(profile.directManagerId()); + respVO.setSupervisorName(profile.directManagerName()); + } + + private void fillPersonalBase(MonthlyReportRespVO respVO, CurrentUserProfile profile) { + respVO.setReporterId(profile.userId()); + respVO.setReporterName(profile.userName()); + respVO.setReporterDeptName(profile.deptName()); + respVO.setReporterPostName(profile.postName()); + respVO.setSupervisorUserId(profile.directManagerId()); + respVO.setSupervisorName(profile.directManagerName()); + } + + private CurrentUserProfile loadCurrentUserProfile(boolean requireManager) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + AdminUserRespDTO user = loadUser(userId); + String userName = StringUtils.hasText(user.getNickname()) + ? user.getNickname().trim() + : safeText(SecurityFrameworkUtils.getLoginUserNickname()); + + String deptName = null; + if (user.getDeptId() != null) { + CommonResult deptResult = deptApi.getDept(user.getDeptId()); + DeptRespDTO dept = deptResult == null ? null : deptResult.getCheckedData(); + deptName = dept == null ? null : dept.getName(); + } + + String postName = null; + if (user.getPositionId() != null) { + Map postMap = postApi.getPostMap(Collections.singleton(user.getPositionId())); + PostRespDTO post = postMap.get(user.getPositionId()); + postName = post == null ? null : post.getName(); + } + + Long directManagerId = null; + String directManagerName = null; + CommonResult directManagerResult = userManagementRelationApi.getDirectManager(userId); + AdminUserRespDTO directManager = directManagerResult == null ? null : directManagerResult.getCheckedData(); + if (directManager != null) { + directManagerId = directManager.getId(); + directManagerName = safeText(directManager.getNickname()); + } + if (requireManager && directManagerId == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_DIRECT_MANAGER_NOT_EXISTS); + } + return new CurrentUserProfile(userId, safeText(userName), deptName, postName, directManagerId, directManagerName); + } + + private AdminUserRespDTO loadUser(Long userId) { + if (userId == null) { + throw invalidParamException("用户编号不能为空"); + } + adminUserApi.validateUserList(Collections.singleton(userId)).getCheckedData(); + CommonResult result = adminUserApi.getUser(userId); + AdminUserRespDTO user = result == null ? null : result.getCheckedData(); + if (user == null) { + throw invalidParamException("用户不存在:{}", userId); + } + return user; + } + + private String resolveProjectManagerName(Long managerUserId) { + if (managerUserId == null) { + return null; + } + AdminUserRespDTO manager = loadUser(managerUserId); + return safeText(manager.getNickname()); + } + + private List buildProjectMemberSnapshot(Long projectId) { + List members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId); + Set userIds = new LinkedHashSet<>(); + for (UserObjectRoleDO member : members) { + if (member == null || !Objects.equals(member.getStatus(), 0) || member.getUserId() == null) { + continue; + } + userIds.add(member.getUserId()); + } + if (userIds.isEmpty()) { + return Collections.emptyList(); + } + Map userMap = adminUserApi.getUserMap(userIds); + List result = new ArrayList<>(userIds.size()); + for (Long userId : userIds) { + WorkReportMemberSnapshotItem item = new WorkReportMemberSnapshotItem(); + item.setUserId(userId); + AdminUserRespDTO user = userMap.get(userId); + item.setUserName(user == null ? "" : safeText(user.getNickname())); + result.add(item); + } + return result; + } + + private Map loadTaskItemTypeLabelMap() { + CommonResult> result = dictDataApi.getDictDataList(DictTypeConstants.RDMS_TASK_ITEM_TYPE); + List dictDataList = result == null ? null : result.getCheckedData(); + if (dictDataList == null || dictDataList.isEmpty()) { + return Collections.emptyMap(); + } + Map labelMap = new LinkedHashMap<>(); + for (DictDataRespDTO item : dictDataList) { + if (item == null || !StringUtils.hasText(item.getValue())) { + continue; + } + labelMap.put(item.getValue().trim(), safeText(item.getLabel())); + } + return labelMap; + } + + private String resolveTaskItemTypeLabel(String typeCode, Map labelMap) { + String normalizedTypeCode = safeText(typeCode); + if (!StringUtils.hasText(normalizedTypeCode)) { + return normalizedTypeCode; + } + String label = labelMap.get(normalizedTypeCode); + return StringUtils.hasText(label) ? label : normalizedTypeCode; + } + + private List loadProgressExcludedTaskStatusCodes() { + return objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE); + } + + private BigDecimal sumReviewWorkHours(List reviewItems) { + if (reviewItems == null || reviewItems.isEmpty()) { + return BigDecimal.ZERO; + } + return reviewItems.stream() + .map(PersonalReportReviewItemRespVO::getWorkHours) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal sumProjectWorkHours(List items) { + if (items == null || items.isEmpty()) { + return BigDecimal.ZERO; + } + return items.stream() + .map(ProjectReportItemRespVO::getWorkHours) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private boolean isZeroProgress(BigDecimal value) { + return value == null || value.compareTo(BigDecimal.ZERO) == 0; + } + + private boolean isCompleted(BigDecimal value) { + return value != null && value.compareTo(BigDecimal.valueOf(100)) >= 0; + } + + private BigDecimal normalizeProgress(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value.stripTrailingZeros(); + } + + private BigDecimal nullToZero(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + private Map loadPlanTaskMap(Long userId, LocalDate nextStartDate, LocalDate nextEndDate, + Set worklogTaskIds) { + Map taskMap = new LinkedHashMap<>(); + projectTaskMapper.selectPlannedInvolvedListByUserIdAndOverlap( + userId, nextStartDate, nextEndDate, ProjectObjectConstants.STATUS_CANCELLED) + .forEach(task -> taskMap.put(task.getId(), task)); + projectTaskMapper.selectInvolvedListByUserIdAndStatusNot(userId, ProjectObjectConstants.STATUS_CANCELLED).stream() + .filter(task -> worklogTaskIds.contains(task.getId()) || isZeroProgress(task.getProgressRate())) + .forEach(task -> taskMap.put(task.getId(), task)); + return taskMap; + } + + private Map loadPlanItemMap(Long userId, LocalDate nextStartDate, LocalDate nextEndDate, + Set worklogTaskIds) { + Map itemMap = new LinkedHashMap<>(); + personalItemMapper.selectPlannedListByOwnerIdAndOverlap( + userId, nextStartDate, nextEndDate, ProjectObjectConstants.STATUS_CANCELLED) + .forEach(item -> itemMap.put(item.getId(), item)); + personalItemMapper.selectListByOwnerIdAndStatusNot(userId, ProjectObjectConstants.STATUS_CANCELLED).stream() + .filter(item -> worklogTaskIds.contains(item.getId()) || isZeroProgress(item.getProgressRate())) + .forEach(item -> itemMap.put(item.getId(), item)); + return itemMap; + } + + private Map aggregateTaskWorklogs(List worklogs) { + if (worklogs == null || worklogs.isEmpty()) { + return Collections.emptyMap(); + } + Map aggregateMap = new LinkedHashMap<>(); + for (TaskWorklogDO worklog : worklogs) { + if (worklog.getTaskId() == null) { + continue; + } + aggregateMap.computeIfAbsent(worklog.getTaskId(), key -> new TaskWorklogAggregate()) + .merge(worklog); + } + return aggregateMap; + } + + private boolean shouldIncludeTaskInPlan(ProjectTaskDO task, LocalDate nextStartDate, LocalDate nextEndDate, + TaskWorklogAggregate worklogAggregate) { + if (task == null || isCompleted(task.getProgressRate())) { + return false; + } + if (worklogAggregate != null) { + return true; + } + if (isZeroProgress(task.getProgressRate())) { + return true; + } + return overlapsNextPeriod(task.getPlannedStartDate(), task.getPlannedEndDate(), nextStartDate, nextEndDate); + } + + private boolean shouldIncludePersonalItemInPlan(PersonalItemDO item, LocalDate nextStartDate, LocalDate nextEndDate, + TaskWorklogAggregate worklogAggregate) { + if (item == null || isCompleted(item.getProgressRate())) { + return false; + } + if (worklogAggregate != null) { + return true; + } + if (isZeroProgress(item.getProgressRate())) { + return true; + } + return overlapsNextPeriod(item.getPlannedStartDate(), item.getPlannedEndDate(), nextStartDate, nextEndDate); + } + + private boolean overlapsNextPeriod(LocalDate plannedStartDate, LocalDate plannedEndDate, + LocalDate nextStartDate, LocalDate nextEndDate) { + return plannedStartDate != null + && plannedEndDate != null + && !plannedStartDate.isAfter(nextEndDate) + && !plannedEndDate.isBefore(nextStartDate); + } + + private String safeText(String value) { + return value == null ? "" : value.trim(); + } + + private LocalDateRange nextWeeklyRange(LocalDate currentStartDate, LocalDate currentEndDate) { + return new LocalDateRange(currentStartDate.plusWeeks(1), currentEndDate.plusWeeks(1)); + } + + private LocalDateRange nextMonthlyRange(LocalDate currentEndDate) { + LocalDate nextMonthStart = currentEndDate.withDayOfMonth(1).plusMonths(1); + return new LocalDateRange(nextMonthStart, nextMonthStart.withDayOfMonth(nextMonthStart.lengthOfMonth())); + } + + private LocalDateRange nextHalfMonthRange(LocalDate currentEndDate) { + LocalDate nextDay = currentEndDate.plusDays(1); + if (nextDay.getDayOfMonth() <= 15) { + return new LocalDateRange(nextDay.withDayOfMonth(1), nextDay.withDayOfMonth(15)); + } + return new LocalDateRange(nextDay.withDayOfMonth(16), nextDay.withDayOfMonth(nextDay.lengthOfMonth())); + } + + private record CurrentUserProfile(Long userId, String userName, String deptName, String postName, + Long directManagerId, String directManagerName) { + } + + private record LocalDateRange(LocalDate startDate, LocalDate endDate) { + } + + private static final class ReviewAggregate { + + private final String workItemTitle; + private final String lineTitle; + private final String priority; + private final String typeCode; + private BigDecimal totalWorkHours = BigDecimal.ZERO; + private BigDecimal latestProgressRate = BigDecimal.ZERO; + private LocalDate latestEndDate; + private final List workContents = new ArrayList<>(); + + private ReviewAggregate(String workItemTitle, String lineTitle, String priority, String typeCode) { + this.workItemTitle = workItemTitle; + this.lineTitle = lineTitle; + this.priority = priority; + this.typeCode = typeCode; + } + + private void merge(TaskWorklogDO worklog) { + totalWorkHours = totalWorkHours.add( + worklog.getDurationHours() == null ? BigDecimal.ZERO : worklog.getDurationHours()); + LocalDate endDate = worklog.getEndDate(); + if (endDate != null && (latestEndDate == null || !endDate.isBefore(latestEndDate))) { + latestEndDate = endDate; + latestProgressRate = worklog.getProgressRate() == null ? BigDecimal.ZERO : worklog.getProgressRate(); + } + if (StringUtils.hasText(worklog.getWorkContent())) { + workContents.add(worklog.getWorkContent().trim()); + } + } + + private String buildWeeklyReviewText() { + StringBuilder builder = new StringBuilder(); + if (StringUtils.hasText(typeCode)) { + builder.append(typeCode).append(" - "); + } + builder.append(lineTitle); + appendBracket(builder, priority, latestProgressRate, totalWorkHours); + if (!workContents.isEmpty()) { + builder.append(":").append(String.join(";", workContents)); + } + return builder.toString(); + } + + private String buildMonthlyReviewText() { + StringBuilder builder = new StringBuilder(); + if (StringUtils.hasText(typeCode)) { + builder.append(typeCode).append(" - "); + } + builder.append(lineTitle); + appendBracket(builder, priority, latestProgressRate, totalWorkHours); + return builder.toString(); + } + + private void appendBracket(StringBuilder builder, String priority, + BigDecimal progressRate, BigDecimal hours) { + List parts = new ArrayList<>(); + if (StringUtils.hasText(priority)) { + parts.add(priority); + } + if (progressRate != null) { + parts.add("进度" + progressRate.stripTrailingZeros().toPlainString() + "%"); + } + if (hours != null) { + parts.add(hours.stripTrailingZeros().toPlainString() + "h"); + } + if (!parts.isEmpty()) { + builder.append("(").append(String.join(" / ", parts)).append(")"); + } + } + + private String workItemTitle() { + return workItemTitle; + } + + private BigDecimal totalWorkHours() { + return totalWorkHours; + } + + private BigDecimal latestProgressRate() { + return latestProgressRate; + } + } + + private static final class PlanAggregate { + + private final String workItemTitle; + private final String category; + private final List lines = new ArrayList<>(); + + private PlanAggregate(String workItemTitle, String category) { + this.workItemTitle = workItemTitle; + this.category = category; + } + + private void addTaskLine(String title, String priority, BigDecimal progressRate, + TaskWorklogAggregate worklogAggregate, boolean weeklyMode) { + if (!StringUtils.hasText(title)) { + return; + } + if (worklogAggregate != null && worklogAggregate.hasWorkContent()) { + lines.add(buildWorklogStyleLine(title, priority, progressRate, worklogAggregate, weeklyMode)); + return; + } + lines.add(buildPlannedStyleLine(title, priority, progressRate)); + } + + private void addItemLine(String title, BigDecimal progressRate, + TaskWorklogAggregate worklogAggregate, boolean weeklyMode) { + if (!StringUtils.hasText(title)) { + return; + } + if (worklogAggregate != null && worklogAggregate.hasWorkContent()) { + lines.add(buildWorklogStyleLine(title, null, progressRate, worklogAggregate, weeklyMode)); + return; + } + lines.add(buildPlannedStyleLine(title, null, progressRate)); + } + + private String buildWorklogStyleLine(String title, String priority, BigDecimal progressRate, + TaskWorklogAggregate worklogAggregate, boolean weeklyMode) { + StringBuilder builder = new StringBuilder(); + if (StringUtils.hasText(category)) { + builder.append(category).append(" - "); + } + builder.append(title); + List parts = new ArrayList<>(); + if (StringUtils.hasText(priority)) { + parts.add(priority); + } + if (progressRate != null) { + parts.add("进度" + progressRate.stripTrailingZeros().toPlainString() + "%"); + } + if (worklogAggregate.totalWorkHours() != null) { + parts.add(worklogAggregate.totalWorkHours().stripTrailingZeros().toPlainString() + "h"); + } + if (!parts.isEmpty()) { + builder.append("(").append(String.join(" / ", parts)).append(")"); + } + if (weeklyMode && worklogAggregate.hasWorkContent()) { + builder.append(":").append(String.join(";", worklogAggregate.workContents())); + } + return builder.toString(); + } + + private String buildPlannedStyleLine(String title, String priority, BigDecimal progressRate) { + StringBuilder builder = new StringBuilder(); + if (StringUtils.hasText(category)) { + builder.append(category).append(" - "); + } + builder.append(title); + List parts = new ArrayList<>(); + if (StringUtils.hasText(priority)) { + parts.add(priority); + } + if (progressRate != null) { + parts.add("进度" + progressRate.stripTrailingZeros().toPlainString() + "%"); + } + if (!parts.isEmpty()) { + builder.append("(").append(String.join(" / ", parts)).append(")"); + } + return builder.toString(); + } + + private String buildTargetText() { + return String.join("\n", lines); + } + + private String workItemTitle() { + return workItemTitle; + } + + private List lines() { + return lines; + } + } + + private static final class TaskWorklogAggregate { + + private BigDecimal totalWorkHours = BigDecimal.ZERO; + private final List workContents = new ArrayList<>(); + + private void merge(TaskWorklogDO worklog) { + totalWorkHours = totalWorkHours.add( + worklog.getDurationHours() == null ? BigDecimal.ZERO : worklog.getDurationHours()); + if (StringUtils.hasText(worklog.getWorkContent())) { + workContents.add(worklog.getWorkContent().trim()); + } + } + + private boolean hasWorkContent() { + return !workContents.isEmpty(); + } + + private BigDecimal totalWorkHours() { + return totalWorkHours; + } + + private List workContents() { + return workContents; + } + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportContentExportService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportContentExportService.java new file mode 100644 index 0000000..cd44aac --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportContentExportService.java @@ -0,0 +1,1583 @@ +package com.njcn.rdms.module.project.service.workreport.export; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.njcn.rdms.framework.common.biz.system.dict.dto.DictDataRespDTO; +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageParam; +import com.njcn.rdms.framework.common.util.json.JsonUtils; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportMemberSnapshotRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportContentExportReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportContentExportReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportItemRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportContentExportReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportTravelSegmentRespVO; +import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; +import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService; +import com.njcn.rdms.module.system.api.dict.DictDataApi; +import jakarta.annotation.Resource; +import org.apache.poi.xwpf.usermodel.*; +import org.apache.xmlbeans.XmlException; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +@Service +public class WorkReportContentExportService { + + private static final String DOCX_CONTENT_TYPE = + "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + private static final String ZIP_CONTENT_TYPE = "application/zip"; + private static final String WEEKLY_TEMPLATE_PATH = "templates/work-report/weekly-report-template.docx"; + private static final String MONTHLY_TEMPLATE_PATH = "templates/work-report/monthly-report-template.docx"; + private static final String PROJECT_TEMPLATE_PATH = "templates/work-report/project-report-template.docx"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm"); + + @Resource + private WorkReportCommonService workReportCommonService; + @Resource + private DictDataApi dictDataApi; + + public WorkReportExportFile exportWeekly(WeeklyReportContentExportReqVO reqVO) throws IOException { + List reports = resolveWeeklyReports(reqVO); + return buildFile("个人周报", reports.stream().map(this::buildWeeklyDocument).toList()); + } + + public WorkReportExportFile exportMonthly(MonthlyReportContentExportReqVO reqVO) throws IOException { + List reports = resolveMonthlyReports(reqVO); + List documents = new ArrayList<>(reports.size()); + for (MonthlyReportRespVO report : reports) { + MonthlyReportApprovalRecordRespVO approvalRecord = latestMonthlyApprovalRecord(report.getId()); + documents.add(buildMonthlyDocument(report, approvalRecord)); + } + return buildFile("个人月报", documents); + } + + public WorkReportExportFile exportProject(ProjectReportContentExportReqVO reqVO) throws IOException { + List reports = resolveProjectReports(reqVO); + return buildFile("项目半月报", reports.stream().map(this::buildProjectDocument).toList()); + } + + private List resolveWeeklyReports(WeeklyReportContentExportReqVO reqVO) { + if (Boolean.TRUE.equals(reqVO.getExportAll())) { + reqVO.setPageNo(1); + reqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + return workReportCommonService.getWeeklyReportPage(reqVO).getList().stream() + .map(item -> workReportCommonService.getWeeklyReport(item.getId())) + .toList(); + } + return distinctIds(reqVO.getIds()).stream() + .map(workReportCommonService::getWeeklyReport) + .toList(); + } + + private List resolveMonthlyReports(MonthlyReportContentExportReqVO reqVO) { + if (Boolean.TRUE.equals(reqVO.getExportAll())) { + reqVO.setPageNo(1); + reqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + return workReportCommonService.getMonthlyReportPage(reqVO).getList().stream() + .map(item -> workReportCommonService.getMonthlyReport(item.getId())) + .toList(); + } + return distinctIds(reqVO.getIds()).stream() + .map(workReportCommonService::getMonthlyReport) + .toList(); + } + + private List resolveProjectReports(ProjectReportContentExportReqVO reqVO) { + if (Boolean.TRUE.equals(reqVO.getExportAll())) { + reqVO.setPageNo(1); + reqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + return workReportCommonService.getProjectReportPage(reqVO).getList().stream() + .map(item -> workReportCommonService.getProjectReport(item.getId())) + .toList(); + } + return distinctIds(reqVO.getIds()).stream() + .map(workReportCommonService::getProjectReport) + .toList(); + } + + private List distinctIds(List ids) { + if (ids == null || ids.isEmpty()) { + throw invalidParamException("请选择要导出的报告"); + } + Set result = new LinkedHashSet<>(); + for (Long id : ids) { + if (id != null) { + result.add(id); + } + } + if (result.isEmpty()) { + throw invalidParamException("请选择要导出的报告"); + } + return new ArrayList<>(result); + } + + private MonthlyReportApprovalRecordRespVO latestMonthlyApprovalRecord(Long reportId) { + List records = workReportCommonService.getMonthlyApprovalRecords(reportId); + if (records == null || records.isEmpty()) { + return null; + } + return records.stream() + .max(Comparator.comparing(MonthlyReportApprovalRecordRespVO::getApprovalRound, + Comparator.nullsFirst(Integer::compareTo))) + .orElse(null); + } + + private WorkReportExportFile buildFile(String title, List documents) throws IOException { + if (documents.isEmpty()) { + throw invalidParamException("没有可导出的报告"); + } + if (documents.size() == 1) { + DocumentContent document = documents.get(0); + return new WorkReportExportFile(document.filename(), DOCX_CONTENT_TYPE, document.content()); + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + for (int i = 0; i < documents.size(); i++) { + DocumentContent document = documents.get(i); + zipOutputStream.putNextEntry(new ZipEntry((i + 1) + "_" + document.filename())); + zipOutputStream.write(document.content()); + zipOutputStream.closeEntry(); + } + } + return new WorkReportExportFile(title + "_" + LocalDate.now() + ".zip", ZIP_CONTENT_TYPE, + outputStream.toByteArray()); + } + + private DocumentContent buildWeeklyDocument(WeeklyReportRespVO report) { + ClassPathResource templateResource = new ClassPathResource(WEEKLY_TEMPLATE_PATH); + if (templateResource.exists()) { + return buildWeeklyTemplateDocument(report, templateResource); + } + return buildWeeklyFallbackDocument(report); + } + + private DocumentContent buildWeeklyTemplateDocument(WeeklyReportRespVO report, ClassPathResource templateResource) { + try (InputStream inputStream = templateResource.getInputStream(); + XWPFDocument document = new XWPFDocument(inputStream)) { + replacePlaceholders(document, buildWeeklyTemplateData(report)); + renderPersonalReviewTable(document, report.getReviewItems()); + renderPersonalPlanTable(document, report.getPlanItems()); + return new DocumentContent(buildFilename("个人周报", report.getReporterName(), report.getPeriodLabel()), + toBytes(document)); + } catch (IOException e) { + throw new IllegalStateException("生成周报模板导出文件失败", e); + } + } + + private DocumentContent buildWeeklyFallbackDocument(WeeklyReportRespVO report) { + try (XWPFDocument document = new XWPFDocument()) { + addTitle(document, "个人周报"); + addParagraph(document, "填报周期:" + formatPeriod(report), false); + addPersonalBaseTable(document, report.getReporterName(), report.getReporterDeptName(), + report.getReporterPostName(), report.getSupervisorName()); + + addSectionTitle(document, "第一部分:当期重点工作回顾"); + addPersonalReviewTable(document, report.getReviewItems(), false); + + if (Boolean.TRUE.equals(report.getIsBusinessTrip())) { + addSectionTitle(document, "出差信息"); + addKeyValueTable(document, List.of( + row("出差天数", formatDecimal(report.getTotalTravelDays())), + row("出差明细", buildTravelText(report.getTravelSegments())))); + } + + addSectionTitle(document, "下周期重点工作计划"); + addPersonalPlanTable(document, report.getPlanItems(), false); + return new DocumentContent(buildFilename("个人周报", report.getReporterName(), report.getPeriodLabel()), + toBytes(document)); + } catch (IOException e) { + throw new IllegalStateException("生成周报导出文件失败", e); + } + } + + private Map buildWeeklyTemplateData(WeeklyReportRespVO report) { + Map data = new HashMap<>(); + data.put("reporterName", text(report.getReporterName())); + data.put("reporterDeptName", text(report.getReporterDeptName())); + data.put("reporterPostName", text(report.getReporterPostName())); + data.put("periodText", formatPeriod(report)); + data.put("periodLabel", text(report.getPeriodLabel())); + data.put("supervisorName", text(report.getSupervisorName())); + data.put("businessTripText", Boolean.TRUE.equals(report.getIsBusinessTrip()) ? "是" : "否"); + data.put("totalTravelDaysText", formatDecimal(report.getTotalTravelDays())); + data.put("totalWorkHoursText", formatDecimal(report.getTotalWorkHours())); + data.put("submitTimeText", formatDateTime(report.getSubmitTime())); + data.put("approvalTimeText", formatDateTime(report.getApprovalTime())); + return data; + } + + private void replacePlaceholders(XWPFDocument document, Map data) { + for (XWPFParagraph paragraph : document.getParagraphs()) { + replaceParagraphPlaceholders(paragraph, data); + } + for (XWPFTable table : document.getTables()) { + for (XWPFTableRow row : table.getRows()) { + for (XWPFTableCell cell : row.getTableCells()) { + for (XWPFParagraph paragraph : cell.getParagraphs()) { + replaceParagraphPlaceholders(paragraph, data); + } + } + } + } + } + + private void replaceParagraphPlaceholders(XWPFParagraph paragraph, Map data) { + List runs = paragraph.getRuns(); + if (runs == null || runs.isEmpty()) { + return; + } + String sourceText = paragraph.getText(); + if (!StringUtils.hasText(sourceText) || !sourceText.contains("{{")) { + return; + } + String replacedText = sourceText; + for (Map.Entry entry : data.entrySet()) { + replacedText = replacedText.replace("{{" + entry.getKey() + "}}", entry.getValue()); + } + XWPFRun firstRun = runs.get(0); + applyRunText(firstRun, replacedText); + for (int i = runs.size() - 1; i >= 1; i--) { + paragraph.removeRun(i); + } + } + + private void renderPersonalReviewTable(XWPFDocument document, List items) { + TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of( + "{{itemNumber}}", "{{itemTitleWithHours}}", "{{contentText}}", "{{reflectionText}}")); + if (location == null) { + return; + } + List> rows = safeList(items).stream() + .map(item -> placeholderValues(Map.of( + "itemNumber", formatNumber(item.getItemNumber()), + "itemTitleWithHours", appendHours(item.getItemTitle(), item.getWorkHours()), + "contentText", buildStructuredReviewText(item, false), + "reflectionText", text(item.getReflectionText())))) + .toList(); + List renderedRows = renderRowsByTemplateRow(location, rows, List.of( + "itemNumber", "itemTitleWithHours", "contentText", "reflectionText")); + rewriteReviewRows(renderedRows, items, false); + } + + private void renderPersonalPlanTable(XWPFDocument document, List items) { + TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of( + "{{itemNumber}}", "{{itemTitle}}", "{{targetText}}", "{{supportNeed}}")); + if (location == null) { + return; + } + List> rows = safeList(items).stream() + .map(item -> placeholderValues(Map.of( + "itemNumber", formatNumber(item.getItemNumber()), + "itemTitle", text(item.getItemTitle()), + "targetText", buildStructuredPlanText(item, false), + "supportNeed", text(item.getSupportNeed())))) + .toList(); + List renderedRows = renderRowsByTemplateRow(location, rows, List.of( + "itemNumber", "itemTitle", "targetText", "supportNeed")); + rewritePlanRows(renderedRows, items, false); + } + + private TemplateRowLocation findTemplateRowByPlaceholders(XWPFDocument document, List placeholders) { + for (XWPFTable table : document.getTables()) { + for (int i = 0; i < table.getRows().size(); i++) { + XWPFTableRow row = table.getRow(i); + String rowXml = row.getCtRow().xmlText(); + if (placeholders.stream().allMatch(rowXml::contains)) { + return new TemplateRowLocation(table, i); + } + } + } + return null; + } + + private List renderRowsByTemplateRow(TemplateRowLocation location, + List> dataRows, List placeholderNames) { + XWPFTable table = location.table(); + int templateRowIndex = location.rowIndex(); + int dataStartIndex = templateRowIndex; + int dataEndIndex = findPlaceholderBlockEndRowIndex(table, templateRowIndex, placeholderNames); + String templateRowXml = table.getRow(templateRowIndex).getCtRow().xmlText(); + for (int i = dataEndIndex - 1; i >= dataStartIndex; i--) { + table.removeRow(i); + } + + List> rows = dataRows == null || dataRows.isEmpty() + ? List.of(emptyPlaceholderValues(placeholderNames)) + : dataRows; + int insertIndex = dataStartIndex; + List insertedRows = new ArrayList<>(rows.size()); + for (Map rowData : rows) { + XWPFTableRow row = new XWPFTableRow(parseRenderedRow(templateRowXml, rowData), table); + table.addRow(row, insertIndex++); + insertedRows.add(table.getRow(insertIndex - 1)); + } + return insertedRows; + } + + private Map placeholderValues(Map values) { + Map result = new HashMap<>(); + for (Map.Entry entry : values.entrySet()) { + result.put("{{" + entry.getKey() + "}}", text(entry.getValue())); + } + return result; + } + + private Map emptyPlaceholderValues(List placeholderNames) { + Map result = new HashMap<>(); + for (String placeholderName : placeholderNames) { + result.put("{{" + placeholderName + "}}", ""); + } + return result; + } + + private CTRow parseRenderedRow(String templateRowXml, Map values) { + String renderedXml = templateRowXml; + for (Map.Entry entry : values.entrySet()) { + renderedXml = renderedXml.replace(entry.getKey(), escapeXmlText(entry.getValue())); + } + try { + return CTRow.Factory.parse(renderedXml); + } catch (XmlException e) { + throw new IllegalStateException("渲染周报模板明细行失败", e); + } + } + + private String escapeXmlText(String value) { + return text(value) + .replace("&", "&") + .replace("<", "<") + .replace(">", ">"); + } + + private int findPlaceholderBlockEndRowIndex(XWPFTable table, int templateRowIndex, List placeholderNames) { + List placeholderTokens = placeholderNames.stream() + .map(name -> "{{" + name + "}}") + .toList(); + for (int i = templateRowIndex; i < table.getRows().size(); i++) { + String rowXml = table.getRow(i).getCtRow().xmlText(); + boolean containsPlaceholder = placeholderTokens.stream().anyMatch(rowXml::contains); + if (!containsPlaceholder) { + return i; + } + } + return table.getNumberOfRows(); + } + + private void applyRunText(XWPFRun run, String value) { + String[] lines = text(value).split("\\R", -1); + run.setText("", 0); + for (int i = 0; i < lines.length; i++) { + if (i == 0) { + run.setText(lines[i], 0); + } else { + run.addBreak(); + run.setText(lines[i]); + } + } + } + + private record TemplateRowLocation(XWPFTable table, int rowIndex) { + } + + private DocumentContent buildMonthlyDocument(MonthlyReportRespVO report, + MonthlyReportApprovalRecordRespVO approvalRecord) { + ClassPathResource templateResource = new ClassPathResource(MONTHLY_TEMPLATE_PATH); + if (templateResource.exists()) { + return buildMonthlyTemplateDocument(report, approvalRecord, templateResource); + } + return buildMonthlyFallbackDocument(report, approvalRecord); + } + + private DocumentContent buildMonthlyTemplateDocument(MonthlyReportRespVO report, + MonthlyReportApprovalRecordRespVO approvalRecord, + ClassPathResource templateResource) { + try (InputStream inputStream = templateResource.getInputStream(); + XWPFDocument document = new XWPFDocument(inputStream)) { + replacePlaceholders(document, buildMonthlyTemplateData(report, approvalRecord)); + renderMonthlyReviewTable(document, report.getReviewItems()); + renderMonthlyFeedbackTables(document, approvalRecord); + renderMonthlyPlanTable(document, report.getPlanItems()); + return new DocumentContent(buildFilename("个人月报", report.getReporterName(), report.getPeriodLabel()), + toBytes(document)); + } catch (IOException e) { + throw new IllegalStateException("生成月报模板导出文件失败", e); + } + } + + private DocumentContent buildMonthlyFallbackDocument(MonthlyReportRespVO report, + MonthlyReportApprovalRecordRespVO approvalRecord) { + try (XWPFDocument document = new XWPFDocument()) { + addTitle(document, "灿能电力绩效反馈面谈表"); + addPersonalBaseTable(document, report.getReporterName(), report.getReporterDeptName(), + report.getReporterPostName(), report.getSupervisorName()); + addKeyValueTable(document, List.of( + row("考核周期", formatPeriod(report)), + row("面谈时间", formatDate(approvalRecord == null ? null : approvalRecord.getMeetingDate())))); + + addSectionTitle(document, "第一部分:当期重点工作回顾(个人填写)"); + addPersonalReviewTable(document, report.getReviewItems(), false); + + addSectionTitle(document, "第二部分:当期工作反馈(直接上级/分管领导填写)"); + addFeedbackTable(document, approvalRecord); + + addSectionTitle(document, "第三部分:当期绩效考核结果"); + addKeyValueTable(document, List.of(row("绩效考核结果", + approvalRecord == null ? null : approvalRecord.getPerformanceResult()))); + + addSectionTitle(document, "第四部分:下周期重点工作计划"); + addPersonalPlanTable(document, report.getPlanItems(), false); + + addParagraph(document, "被考核人签名:" + text(approvalRecord == null ? null : approvalRecord.getEmployeeSignName()) + + " 日期:" + formatDate(approvalRecord == null ? null : approvalRecord.getEmployeeSignedDate()), false); + addParagraph(document, "上级签名:" + text(approvalRecord == null ? null : approvalRecord.getSupervisorSignName()) + + " 日期:" + formatDate(approvalRecord == null ? null : approvalRecord.getSupervisorSignedDate()), false); + return new DocumentContent(buildFilename("个人月报", report.getReporterName(), report.getPeriodLabel()), + toBytes(document)); + } catch (IOException e) { + throw new IllegalStateException("生成月报导出文件失败", e); + } + } + + private DocumentContent buildProjectDocument(ProjectReportRespVO report) { + ClassPathResource templateResource = new ClassPathResource(PROJECT_TEMPLATE_PATH); + if (templateResource.exists()) { + return buildProjectTemplateDocument(report, templateResource); + } + return buildProjectFallbackDocument(report); + } + + private DocumentContent buildProjectTemplateDocument(ProjectReportRespVO report, + ClassPathResource templateResource) { + try (InputStream inputStream = templateResource.getInputStream(); + XWPFDocument document = new XWPFDocument(inputStream)) { + replacePlaceholders(document, buildProjectTemplateData(report)); + return new DocumentContent(buildFilename("项目半月报", report.getProjectName(), report.getPeriodLabel()), + toBytes(document)); + } catch (IOException e) { + throw new IllegalStateException("生成项目半月报模板导出文件失败", e); + } + } + + private DocumentContent buildProjectFallbackDocument(ProjectReportRespVO report) { + try (XWPFDocument document = new XWPFDocument()) { + addTitle(document, "研发工作情况简报"); + addParagraph(document, "研发工作情况简报(" + formatPeriod(report) + "):", true); + addSectionTitle(document, "一、" + text(report.getProjectName())); + addKeyValueTable(document, List.of( + row("项目名称", report.getProjectName()), + row("项目负责人", report.getProjectOwnerName()), + row("技术负责人", report.getTechnicalOwnerName()), + row("项目组成员", buildMemberText(report.getProjectMemberSnapshot())), + row("项目状况", report.getProjectStatusDesc()), + row("整体计划进度", report.getProjectProgressPlan()))); + + addSectionTitle(document, "本期工作内容"); + addProjectItemTable(document, report.getCurrentItems(), true); + + addSectionTitle(document, "下期计划工作内容"); + addProjectItemTable(document, report.getNextItems(), false); + + addKeyValueTable(document, List.of( + row("要点描述", report.getProjectKeyPoints()), + row("项目问题", report.getProjectProblems()))); + return new DocumentContent(buildFilename("项目半月报", report.getProjectName(), report.getPeriodLabel()), + toBytes(document)); + } catch (IOException e) { + throw new IllegalStateException("生成项目半月报导出文件失败", e); + } + } + + private Map buildMonthlyTemplateData(MonthlyReportRespVO report, + MonthlyReportApprovalRecordRespVO approvalRecord) { + Map data = new HashMap<>(); + data.put("reporterName", text(report.getReporterName())); + data.put("reporterDeptName", text(report.getReporterDeptName())); + data.put("reporterPostName", text(report.getReporterPostName())); + data.put("periodText", formatPeriod(report)); + data.put("supervisorName", text(report.getSupervisorName())); + data.put("meetingDateText", formatDate(approvalRecord == null ? null : approvalRecord.getMeetingDate())); + data.put("strengthDesc", text(approvalRecord == null ? null : approvalRecord.getStrengthDesc())); + data.put("strengthExample", text(approvalRecord == null ? null : approvalRecord.getStrengthExample())); + data.put("weaknessDesc", text(approvalRecord == null ? null : approvalRecord.getWeaknessDesc())); + data.put("weaknessExample", text(approvalRecord == null ? null : approvalRecord.getWeaknessExample())); + data.put("improvementSuggestion", text(approvalRecord == null ? null : approvalRecord.getImprovementSuggestion())); + data.put("performanceResult", text(approvalRecord == null ? null : approvalRecord.getPerformanceResult())); + data.put("employeeSignName", text(approvalRecord == null ? null : approvalRecord.getEmployeeSignName())); + data.put("employeeSignedDateText", formatDate(approvalRecord == null ? null : approvalRecord.getEmployeeSignedDate())); + data.put("supervisorSignName", text(approvalRecord == null ? null : approvalRecord.getSupervisorSignName())); + data.put("supervisorSignedDateText", formatDate(approvalRecord == null ? null : approvalRecord.getSupervisorSignedDate())); + return data; + } + + private Map buildProjectTemplateData(ProjectReportRespVO report) { + Map data = new HashMap<>(); + data.put("briefDateText", report.getPeriodEndDate() == null ? "" : + report.getPeriodEndDate().format(DateTimeFormatter.ofPattern("yyyyMMdd"))); + data.put("periodText", formatPeriod(report)); + data.put("projectName", text(report.getProjectName())); + data.put("projectOwnerName", text(report.getProjectOwnerName())); + data.put("technicalOwnerName", text(report.getTechnicalOwnerName())); + data.put("projectMembersText", buildMemberText(report.getProjectMemberSnapshot())); + data.put("projectStatusDesc", text(report.getProjectStatusDesc())); + data.put("projectProgressPlan", text(report.getProjectProgressPlan())); + data.put("currentItems", buildProjectItemsText(report.getCurrentItems(), true)); + data.put("nextItems", buildProjectItemsText(report.getNextItems(), false)); + data.put("projectKeyPoints", text(report.getProjectKeyPoints())); + data.put("projectProblems", text(report.getProjectProblems())); + return data; + } + + private void renderMonthlyReviewTable(XWPFDocument document, List items) { + TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of( + "{{itemNumber}}", "{{itemTitleWithHours}}", "{{contentText}}", "{{reflectionText}}")); + if (location == null) { + return; + } + List> rows = safeList(items).stream() + .map(item -> placeholderValues(Map.of( + "itemNumber", formatNumber(item.getItemNumber()), + "itemTitleWithHours", appendHours(item.getItemTitle(), item.getWorkHours()), + "contentText", buildStructuredReviewText(item, false), + "reflectionText", text(item.getReflectionText())))) + .toList(); + List renderedRows = renderRowsByTemplateRow(location, rows, List.of( + "itemNumber", "itemTitleWithHours", "contentText", "reflectionText")); + rewriteReviewRows(renderedRows, items, false); + } + + private void renderMonthlyPlanTable(XWPFDocument document, List items) { + TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of( + "{{itemNumber}}", "{{itemTitle}}", "{{targetText}}", "{{supportNeed}}")); + if (location == null) { + return; + } + List> rows = safeList(items).stream() + .map(item -> placeholderValues(Map.of( + "itemNumber", formatNumber(item.getItemNumber()), + "itemTitle", text(item.getItemTitle()), + "targetText", buildStructuredPlanText(item, false), + "supportNeed", text(item.getSupportNeed())))) + .toList(); + List renderedRows = renderRowsByTemplateRow(location, rows, List.of( + "itemNumber", "itemTitle", "targetText", "supportNeed")); + rewritePlanRows(renderedRows, items, false); + } + + private void renderMonthlyFeedbackTables(XWPFDocument document, MonthlyReportApprovalRecordRespVO approvalRecord) { + TemplateRowLocation strengthLocation = findTemplateRowByPlaceholders(document, List.of( + "优势", "{{strengthDesc}}", "{{strengthExample}}", "{{improvementSuggestion}}")); + if (strengthLocation != null) { + renderSingleTemplateRow(strengthLocation, placeholderValues(Map.of( + "strengthDesc", text(approvalRecord == null ? null : approvalRecord.getStrengthDesc()), + "strengthExample", text(approvalRecord == null ? null : approvalRecord.getStrengthExample()), + "improvementSuggestion", text(approvalRecord == null ? null : approvalRecord.getImprovementSuggestion()) + ))); + } + TemplateRowLocation weaknessLocation = findTemplateRowByPlaceholders(document, List.of( + "劣势", "{{weaknessDesc}}", "{{weaknessExample}}")); + if (weaknessLocation != null) { + renderSingleTemplateRow(weaknessLocation, placeholderValues(Map.of( + "weaknessDesc", text(approvalRecord == null ? null : approvalRecord.getWeaknessDesc()), + "weaknessExample", text(approvalRecord == null ? null : approvalRecord.getWeaknessExample()) + ))); + } + } + + private void renderSingleTemplateRow(TemplateRowLocation location, Map values) { + XWPFTableRow row = location.table().getRow(location.rowIndex()); + CTRow renderedRow = parseRenderedRow(row.getCtRow().xmlText(), values); + row.getCtRow().set(renderedRow); + } + + private void rewriteReviewRows(List rows, List items, + boolean includeDetail) { + List safeItems = safeList(items); + for (int i = 0; i < rows.size() && i < safeItems.size(); i++) { + PersonalReportReviewItemRespVO item = safeItems.get(i); + XWPFTableRow row = rows.get(i); + setStructuredReviewCell(row.getCell(2), item, includeDetail); + setCellText(row.getCell(3), text(item.getReflectionText()), false); + } + } + + private void rewritePlanRows(List rows, List items, + boolean includeDetail) { + List safeItems = safeList(items); + for (int i = 0; i < rows.size() && i < safeItems.size(); i++) { + PersonalReportPlanItemRespVO item = safeItems.get(i); + XWPFTableRow row = rows.get(i); + setStructuredPlanCell(row.getCell(2), item, includeDetail); + setCellText(row.getCell(3), text(item.getSupportNeed()), false); + } + } + + private String buildStructuredReviewText(PersonalReportReviewItemRespVO item, boolean includeDetail) { + return buildStructuredText(item == null ? null : item.getContentJson(), + item == null ? null : item.getContentText(), true, includeDetail); + } + + private String buildStructuredPlanText(PersonalReportPlanItemRespVO item, boolean includeDetail) { + return buildStructuredText(item == null ? null : item.getTargetJson(), + item == null ? null : item.getTargetText(), false, includeDetail); + } + + private void setStructuredReviewCell(XWPFTableCell cell, PersonalReportReviewItemRespVO item, + boolean includeDetail) { + setStructuredCellTextForTemplate(cell, + item == null ? null : item.getContentJson(), + item == null ? null : item.getContentText(), + true, + includeDetail); + } + + private void setStructuredPlanCell(XWPFTableCell cell, PersonalReportPlanItemRespVO item, + boolean includeDetail) { + setStructuredCellTextForTemplate(cell, + item == null ? null : item.getTargetJson(), + item == null ? null : item.getTargetText(), + false, + includeDetail); + } + + private void setStructuredCellTextForTemplate(XWPFTableCell cell, Object jsonValue, + String fallbackText, boolean includeHours, + boolean includeDetail) { + List lines = buildStructuredParagraphsForTemplate(jsonValue, fallbackText, includeHours, includeDetail); + unsetNoWrap(cell); + if (lines.isEmpty()) { + setCellTextByTemplateParagraph(cell, normalizeLineBreaks(fallbackText), false); + return; + } + setCellTextByTemplateParagraph(cell, String.join("\n", lines), false); + } + + private List buildStructuredParagraphsForTemplate(Object jsonValue, String fallbackText, + boolean includeHours, boolean includeDetail) { + List sections = parseStructuredSections(jsonValue); + if (sections.isEmpty()) { + sections = parseStructuredSectionsForTemplate(fallbackText); + } + if (sections.isEmpty()) { + return List.of(); + } + Map priorityLabelMap = loadPriorityLabelMap(); + List lines = new ArrayList<>(); + for (StructuredSectionData section : sections) { + String category = normalizeSectionCategory(section.category()); + if (StringUtils.hasText(category)) { + lines.add("# " + category + "\uFF1A"); + } + List tasks = safeList(section.tasks()); + for (int i = 0; i < tasks.size(); i++) { + StructuredTaskData task = tasks.get(i); + String taskLine = (i + 1) + "\u3001 " + formatStructuredTaskForTemplate(task, includeHours, priorityLabelMap); + String detail = includeDetail ? text(task.detail()) : ""; + if (StringUtils.hasText(detail)) { + List detailLines = splitTextLines(detail); + if (detailLines.isEmpty()) { + lines.add(taskLine + "\uFF1A"); + } else { + lines.add(taskLine + "\uFF1A" + detailLines.get(0)); + for (int j = 1; j < detailLines.size(); j++) { + lines.add(detailLines.get(j)); + } + } + } else { + lines.add(taskLine); + } + } + } + while (!lines.isEmpty() && !StringUtils.hasText(lines.get(lines.size() - 1))) { + lines.remove(lines.size() - 1); + } + return lines; + } + + private String formatStructuredTaskForTemplate(StructuredTaskData task, boolean includeHours, + Map priorityLabelMap) { + List metrics = new ArrayList<>(); + if (StringUtils.hasText(task.priority())) { + metrics.add(resolvePriorityLabel(task.priority(), priorityLabelMap)); + } + if (task.progress() != null) { + metrics.add("\u8FDB\u5EA6 " + formatDecimal(task.progress()) + "%"); + } + if (includeHours && task.hours() != null) { + metrics.add(formatDecimal(task.hours()) + "h"); + } + return text(task.title()) + (metrics.isEmpty() ? "" : "\uFF08" + String.join(" / ", metrics) + "\uFF09"); + } + + private List parseStructuredSectionsForTemplate(String textValue) { + if (!StringUtils.hasText(textValue)) { + return List.of(); + } + String[] rawLines = normalizeLineBreaks(text(textValue)).split("\\R"); + Map> sectionMap = new LinkedHashMap<>(); + String currentCategory = null; + for (String rawLine : rawLines) { + String line = text(rawLine); + if (!StringUtils.hasText(line)) { + continue; + } + if (line.startsWith("#")) { + currentCategory = normalizeSectionCategory(line.substring(1)); + if (StringUtils.hasText(currentCategory)) { + sectionMap.putIfAbsent(currentCategory, new ArrayList<>()); + } + continue; + } + if (appendDetailContinuation(sectionMap, currentCategory, line)) { + continue; + } + java.util.regex.Matcher legacyMatcher = java.util.regex.Pattern + .compile("^(.+?)\\s*[--]\\s*(.+)$") + .matcher(line); + if (legacyMatcher.matches()) { + String category = normalizeSectionCategory(legacyMatcher.group(1)); + StructuredTaskData task = parseTemplateStructuredTask(legacyMatcher.group(2)); + if (StringUtils.hasText(category) && task != null) { + sectionMap.computeIfAbsent(category, key -> new ArrayList<>()).add(task); + continue; + } + } + StructuredTaskData task = parseTemplateStructuredTask(line); + if (task == null) { + continue; + } + String category = StringUtils.hasText(currentCategory) ? currentCategory : "工作内容"; + sectionMap.computeIfAbsent(category, key -> new ArrayList<>()).add(task); + } + return sectionMap.entrySet().stream() + .map(entry -> new StructuredSectionData(entry.getKey(), entry.getValue())) + .toList(); + } + + private StructuredTaskData parseTemplateStructuredTask(String line) { + String normalized = text(line) + .replaceFirst("^\\d+[.、.]\\s*", "") + .replaceFirst("[。.!!?]+$", ""); + if (!StringUtils.hasText(normalized)) { + return null; + } + TaskDetailParts parts = splitTaskDetail(normalized); + java.util.regex.Matcher matcher = java.util.regex.Pattern + .compile("^(.*?)(?:[((]([^()()]*)[))])?$") + .matcher(parts.title()); + if (!matcher.matches()) { + return new StructuredTaskData(parts.title(), parts.detail(), null, null, null); + } + String title = text(matcher.group(1)); + if (!StringUtils.hasText(title)) { + return null; + } + StructuredMetricsData metrics = parseTemplateMetrics(text(matcher.group(2))); + return new StructuredTaskData(title, parts.detail(), metrics.priority(), metrics.progress(), metrics.hours()); + } + + private StructuredMetricsData parseTemplateMetrics(String metricsText) { + if (!StringUtils.hasText(metricsText)) { + return new StructuredMetricsData(null, null, null); + } + String[] parts = metricsText.split("[//]"); + String priority = null; + BigDecimal progress = null; + BigDecimal hours = null; + for (String part : parts) { + String trimmed = text(part); + if (!StringUtils.hasText(trimmed)) { + continue; + } + java.util.regex.Matcher priorityMatcher = java.util.regex.Pattern + .compile("(?i)^P?(\\d+)(?:\\.0+)?$") + .matcher(trimmed); + if (priority == null && priorityMatcher.matches()) { + priority = normalizePriority(priorityMatcher.group(1)); + continue; + } + java.util.regex.Matcher progressMatcher = java.util.regex.Pattern + .compile("^(?:进度\\s*)?(\\d+(?:\\.\\d+)?)%$") + .matcher(trimmed); + if (progress == null && progressMatcher.matches()) { + progress = parseDecimal(progressMatcher.group(1)); + continue; + } + java.util.regex.Matcher hoursMatcher = java.util.regex.Pattern + .compile("^(\\d+(?:\\.\\d+)?)h$") + .matcher(trimmed); + if (hours == null && hoursMatcher.matches()) { + hours = parseDecimal(hoursMatcher.group(1)); + } + } + return new StructuredMetricsData(priority, progress, hours); + } + + private String buildStructuredText(Object jsonValue, String fallbackText, boolean includeHours, + boolean includeDetail) { + List sections = parseStructuredSections(jsonValue); + if (sections.isEmpty()) { + sections = parseStructuredSectionsFromText(fallbackText); + } + if (sections.isEmpty()) { + return text(fallbackText); + } + Map priorityLabelMap = loadPriorityLabelMap(); + List lines = new ArrayList<>(); + for (StructuredSectionData section : sections) { + lines.add("# " + normalizeSectionCategory(section.category()) + ":"); + List tasks = safeList(section.tasks()); + for (int i = 0; i < tasks.size(); i++) { + StructuredTaskData task = tasks.get(i); + String taskLine = (i + 1) + "、" + formatStructuredTask(task, includeHours, priorityLabelMap); + String detail = includeDetail ? text(task.detail()) : ""; + if (StringUtils.hasText(detail)) { + List detailLines = splitTextLines(detail); + if (detailLines.isEmpty()) { + lines.add(taskLine + ":"); + } else { + lines.add(taskLine + ":" + detailLines.get(0)); + for (int j = 1; j < detailLines.size(); j++) { + lines.add(detailLines.get(j)); + } + } + } else { + lines.add(taskLine); + } + } + } + return String.join("\n", lines); + } + + private void setStructuredCellText(XWPFTableCell cell, Object jsonValue, String fallbackText, boolean includeHours) { + List lines = buildStructuredLines(jsonValue, fallbackText, includeHours); + if (lines.isEmpty()) { + setCellText(cell, fallbackText, false); + return; + } + setCellParagraphs(cell, lines, false); + } + + private List buildStructuredLines(Object jsonValue, String fallbackText, boolean includeHours) { + List sections = parseStructuredSections(jsonValue); + if (sections.isEmpty()) { + sections = parseStructuredSectionsFromText(fallbackText); + } + if (sections.isEmpty()) { + return List.of(); + } + Map priorityLabelMap = loadPriorityLabelMap(); + List lines = new ArrayList<>(); + for (StructuredSectionData section : sections) { + lines.add("# " + normalizeSectionCategory(section.category()) + ":"); + List tasks = safeList(section.tasks()); + for (int i = 0; i < tasks.size(); i++) { + lines.add((i + 1) + "、" + formatStructuredTask(tasks.get(i), includeHours, priorityLabelMap)); + } + } + return lines; + } + + private List parseStructuredSections(Object jsonValue) { + if (jsonValue == null) { + return List.of(); + } + Object normalized = jsonValue; + if (jsonValue instanceof String textValue && StringUtils.hasText(textValue) && JsonUtils.isJson(textValue)) { + normalized = JsonUtils.parseTree(textValue); + } + if (normalized instanceof Map mapValue) { + Object sectionsValue = mapValue.get("sections"); + if (sectionsValue instanceof List sectionList) { + return convertStructuredSections(sectionList); + } + } + if (normalized instanceof List sectionList) { + return convertStructuredSections(sectionList); + } + try { + Map mapValue = JsonUtils.convertObject(normalized, new TypeReference>() {}); + Object sectionsValue = mapValue == null ? null : mapValue.get("sections"); + if (sectionsValue instanceof List sectionList) { + return convertStructuredSections(sectionList); + } + } catch (IllegalArgumentException ignored) { + return List.of(); + } + return List.of(); + } + + private List convertStructuredSections(List rawSections) { + List result = new ArrayList<>(); + for (Object rawSection : rawSections) { + Map sectionMap; + try { + sectionMap = JsonUtils.convertObject(rawSection, new TypeReference>() {}); + } catch (IllegalArgumentException ignored) { + continue; + } + if (sectionMap == null) { + continue; + } + String category = textOf(firstNonNull(sectionMap.get("category"), sectionMap.get("title"), sectionMap.get("name"))); + Object tasksValue = sectionMap.get("tasks"); + List tasks = new ArrayList<>(); + if (tasksValue instanceof List rawTasks) { + for (Object rawTask : rawTasks) { + Map taskMap; + try { + taskMap = JsonUtils.convertObject(rawTask, new TypeReference>() {}); + } catch (IllegalArgumentException ignored) { + continue; + } + if (taskMap == null) { + continue; + } + String title = textOf(firstNonNull(taskMap.get("title"), taskMap.get("name"))); + if (!StringUtils.hasText(title)) { + continue; + } + tasks.add(new StructuredTaskData( + title, + textOf(firstNonNull(taskMap.get("detail"), taskMap.get("content"))), + textOf(taskMap.get("priority")), + parseDecimal(taskMap.get("progress")), + parseDecimal(taskMap.get("hours")))); + } + } + if (StringUtils.hasText(category) || !tasks.isEmpty()) { + result.add(new StructuredSectionData(StringUtils.hasText(category) ? category : "工作内容", tasks)); + } + } + return result; + } + + private List parseStructuredSectionsFromText(String textValue) { + if (!StringUtils.hasText(textValue)) { + return List.of(); + } + String[] lines = normalizeLineBreaks(text(textValue)).split("\\R"); + Map> sectionMap = new LinkedHashMap<>(); + String currentCategory = null; + for (String line : lines) { + String trimmed = text(line); + if (!StringUtils.hasText(trimmed)) { + continue; + } + if (trimmed.startsWith("#")) { + currentCategory = normalizeSectionCategory(trimmed.replaceFirst("^#\\s*", "")); + sectionMap.putIfAbsent(currentCategory, new ArrayList<>()); + continue; + } + if (appendDetailContinuation(sectionMap, currentCategory, trimmed)) { + continue; + } + StructuredLineData legacyLine = parseLegacyStructuredLine(trimmed); + if (legacyLine != null) { + sectionMap.computeIfAbsent(legacyLine.category(), key -> new ArrayList<>()).add(legacyLine.task()); + continue; + } + StructuredTaskData task = parseSimpleStructuredLine(trimmed); + if (task != null) { + sectionMap.computeIfAbsent(StringUtils.hasText(currentCategory) ? currentCategory : "工作内容", + key -> new ArrayList<>()).add(task); + } + } + return sectionMap.entrySet().stream() + .map(entry -> new StructuredSectionData(normalizeSectionCategory(entry.getKey()), entry.getValue())) + .toList(); + } + + private StructuredLineData parseLegacyStructuredLine(String line) { + java.util.regex.Matcher matcher = java.util.regex.Pattern + .compile("^(.+?)\\s*[--]\\s*(.+)$") + .matcher(line); + if (!matcher.matches()) { + return null; + } + String category = text(matcher.group(1)); + StructuredTaskData task = parseSimpleStructuredLine(matcher.group(2)); + if (!StringUtils.hasText(category) || task == null) { + return null; + } + return new StructuredLineData(category, task); + } + + private StructuredTaskData parseSimpleStructuredLine(String line) { + String normalized = text(line).replaceFirst("^\\d+[..、]\\s*", ""); + if (!StringUtils.hasText(normalized)) { + return null; + } + TaskDetailParts parts = splitTaskDetail(normalized); + java.util.regex.Matcher matcher = java.util.regex.Pattern + .compile("^(.*?)(?:[((]([^()()]*)[))])?[。.!!??]*$") + .matcher(parts.title()); + if (!matcher.matches()) { + return new StructuredTaskData(parts.title(), parts.detail(), null, null, null); + } + String title = text(matcher.group(1)); + if (!StringUtils.hasText(title)) { + return null; + } + StructuredMetricsData metrics = parseMetrics(text(matcher.group(2))); + return new StructuredTaskData(title, parts.detail(), metrics.priority(), metrics.progress(), metrics.hours()); + } + + private StructuredMetricsData parseMetrics(String metricsText) { + if (!StringUtils.hasText(metricsText)) { + return new StructuredMetricsData(null, null, null); + } + String[] parts = metricsText.split("/"); + String priority = null; + BigDecimal progress = null; + BigDecimal hours = null; + for (String part : parts) { + String trimmed = text(part); + if (!StringUtils.hasText(trimmed)) { + continue; + } + if (priority == null && trimmed.matches("(?i)^P?\\d+$")) { + priority = normalizePriority(trimmed); + continue; + } + if (progress == null && trimmed.matches("^\\d+(?:\\.\\d+)?%$")) { + progress = parseDecimal(trimmed.substring(0, trimmed.length() - 1)); + continue; + } + if (progress == null && trimmed.matches("^进度\\s*\\d+(?:\\.\\d+)?%$")) { + progress = parseDecimal(trimmed.replaceFirst("^进度\\s*", "").replace("%", "")); + continue; + } + if (hours == null && trimmed.matches("^\\d+(?:\\.\\d+)?h$")) { + hours = parseDecimal(trimmed.substring(0, trimmed.length() - 1)); + } + } + return new StructuredMetricsData(priority, progress, hours); + } + + private String formatStructuredTask(StructuredTaskData task, boolean includeHours, Map priorityLabelMap) { + List metrics = new ArrayList<>(); + if (StringUtils.hasText(task.priority())) { + metrics.add(resolvePriorityLabel(task.priority(), priorityLabelMap)); + } + if (task.progress() != null) { + metrics.add("\u8FDB\u5EA6 " + formatDecimal(task.progress()) + "%"); + } + if (includeHours && task.hours() != null) { + metrics.add(formatDecimal(task.hours()) + "h"); + } + return text(task.title()) + (metrics.isEmpty() ? "" : "(" + String.join(" / ", metrics) + ")"); + } + + private String normalizePriority(String value) { + String trimmed = text(value); + if (!StringUtils.hasText(trimmed)) { + return ""; + } + if (trimmed.matches("^\\d+\\.0+$")) { + trimmed = trimmed.substring(0, trimmed.indexOf('.')); + } + if (trimmed.matches("(?i)^P\\d+$")) { + return trimmed.toUpperCase(); + } + if (trimmed.matches("^\\d+$")) { + return "P" + trimmed; + } + return trimmed; + } + + private String resolvePriorityLabel(String value, Map priorityLabelMap) { + String normalized = normalizePriority(value); + if (!StringUtils.hasText(normalized)) { + return ""; + } + if (normalized.matches("(?i)^P\\d+$")) { + return normalized.toUpperCase(); + } + String rawValue = text(value); + String label = priorityLabelMap.get(rawValue); + if (!StringUtils.hasText(label) && rawValue.matches("^\\d+\\.0+$")) { + label = priorityLabelMap.get(rawValue.substring(0, rawValue.indexOf('.'))); + } + if (!StringUtils.hasText(label)) { + label = priorityLabelMap.get(normalized.replaceFirst("(?i)^P", "")); + } + if (!StringUtils.hasText(label)) { + label = priorityLabelMap.get(normalized); + } + return StringUtils.hasText(label) ? label : normalized; + } + + private boolean appendDetailContinuation(Map> sectionMap, + String currentCategory, String line) { + if (looksLikeStructuredTaskStart(line)) { + return false; + } + String category = StringUtils.hasText(currentCategory) ? currentCategory : lastCategory(sectionMap); + if (!StringUtils.hasText(category)) { + return false; + } + List tasks = sectionMap.get(category); + if (tasks == null || tasks.isEmpty()) { + return false; + } + int lastIndex = tasks.size() - 1; + StructuredTaskData lastTask = tasks.get(lastIndex); + if (!StringUtils.hasText(lastTask.detail())) { + return false; + } + tasks.set(lastIndex, new StructuredTaskData(lastTask.title(), + lastTask.detail() + "\n" + line, + lastTask.priority(), lastTask.progress(), lastTask.hours())); + return true; + } + + private boolean looksLikeStructuredTaskStart(String line) { + String value = text(line); + return value.startsWith("#") + || value.matches("^\\d+[..、]\\s*.+") + || value.matches("^.+?\\s*[--]\\s*.+"); + } + + private String lastCategory(Map> sectionMap) { + if (sectionMap == null || sectionMap.isEmpty()) { + return null; + } + String result = null; + for (String category : sectionMap.keySet()) { + result = category; + } + return result; + } + + private TaskDetailParts splitTaskDetail(String value) { + String source = text(value); + if (!StringUtils.hasText(source)) { + return new TaskDetailParts("", ""); + } + java.util.regex.Matcher matcher = java.util.regex.Pattern + .compile("^(.*?)(?:[::]\\s*)(.*)$") + .matcher(source); + if (!matcher.matches()) { + return new TaskDetailParts(source, ""); + } + return new TaskDetailParts(text(matcher.group(1)), text(matcher.group(2))); + } + + private List splitTextLines(String value) { + String normalized = normalizeLineBreaks(text(value)); + if (!StringUtils.hasText(normalized)) { + return List.of(); + } + List result = new ArrayList<>(); + for (String line : normalized.split("\\R")) { + String textLine = text(line); + if (StringUtils.hasText(textLine)) { + result.add(textLine); + } + } + return result; + } + + private String normalizeLineBreaks(String value) { + return text(value) + .replace("\\r\\n", "\n") + .replace("\\n", "\n") + .replace("\\r", "\n"); + } + + private Map loadPriorityLabelMap() { + CommonResult> result = dictDataApi.getDictDataList(ProjectDictTypeConstants.REQ_PRIORITY); + List dictDataList = result == null ? null : result.getCheckedData(); + if (dictDataList == null || dictDataList.isEmpty()) { + return Map.of(); + } + Map labelMap = new HashMap<>(); + for (DictDataRespDTO item : dictDataList) { + if (item == null) { + continue; + } + String value = text(item.getValue()); + String label = text(item.getLabel()); + if (StringUtils.hasText(value) && StringUtils.hasText(label)) { + labelMap.put(value, label); + } + } + return labelMap; + } + + private String normalizeSectionCategory(String category) { + return text(category).replaceFirst("[::]+$", ""); + } + + private BigDecimal parseDecimal(Object value) { + if (value == null) { + return null; + } + if (value instanceof BigDecimal decimal) { + return decimal; + } + if (value instanceof Number number) { + return BigDecimal.valueOf(number.doubleValue()); + } + if (value instanceof String textValue && StringUtils.hasText(textValue)) { + try { + return new BigDecimal(textValue.trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } + + private Object firstNonNull(Object... values) { + if (values == null) { + return null; + } + for (Object value : values) { + if (value != null) { + return value; + } + } + return null; + } + + private String buildProjectItemsText(List items, boolean includeHours) { + if (items == null || items.isEmpty()) { + return ""; + } + Map priorityLabelMap = loadPriorityLabelMap(); + List lines = new ArrayList<>(items.size()); + int index = 1; + for (ProjectReportItemRespVO item : items) { + String itemTitle = text(item == null ? null : item.getItemTitle()); + if (!StringUtils.hasText(itemTitle)) { + continue; + } + List metrics = new ArrayList<>(); + if (item != null && StringUtils.hasText(item.getPriorityCode())) { + metrics.add(resolvePriorityLabel(item.getPriorityCode(), priorityLabelMap)); + } + if (item != null && item.getProgressRate() != null) { + metrics.add(formatProgress(item.getProgressRate())); + } + if (includeHours && item != null && item.getWorkHours() != null) { + metrics.add(formatDecimal(item.getWorkHours()) + "h"); + } + String suffix = metrics.isEmpty() ? "" : "(" + String.join("/", metrics) + ")"; + lines.add(index++ + "、" + itemTitle + suffix + "。"); + } + return String.join("\n", lines); + } + + private void addTitle(XWPFDocument document, String title) { + XWPFParagraph paragraph = document.createParagraph(); + paragraph.setAlignment(ParagraphAlignment.CENTER); + XWPFRun run = paragraph.createRun(); + run.setBold(true); + run.setFontSize(18); + run.setText(title); + } + + private void addSectionTitle(XWPFDocument document, String title) { + XWPFParagraph paragraph = document.createParagraph(); + XWPFRun run = paragraph.createRun(); + run.setBold(true); + run.setFontSize(12); + run.setText(title); + } + + private void addParagraph(XWPFDocument document, String text, boolean bold) { + XWPFParagraph paragraph = document.createParagraph(); + XWPFRun run = paragraph.createRun(); + run.setBold(bold); + run.setText(text(text)); + } + + private void addPersonalBaseTable(XWPFDocument document, String name, String deptName, + String postName, String managerName) { + addKeyValueTable(document, List.of( + row("姓名", name), + row("部门", deptName), + row("岗位", postName), + row("直接上级", managerName))); + } + + private void addPersonalReviewTable(XWPFDocument document, List items, + boolean includeDetail) { + XWPFTable table = document.createTable(1, 4); + setTableWidth(table); + setRowText(table.getRow(0), List.of("序号", "工作事项", "具体工作内容及成果描述", "工作感悟"), true); + for (PersonalReportReviewItemRespVO item : safeList(items)) { + XWPFTableRow row = table.createRow(); + setCellText(row.getCell(0), formatNumber(item.getItemNumber()), false); + setCellText(row.getCell(1), appendHours(item.getItemTitle(), item.getWorkHours()), false); + setCellText(row.getCell(2), buildStructuredReviewText(item, includeDetail), false); + setCellText(row.getCell(3), item.getReflectionText(), false); + } + } + + private void addPersonalPlanTable(XWPFDocument document, List items, + boolean includeDetail) { + XWPFTable table = document.createTable(1, 4); + setTableWidth(table); + setRowText(table.getRow(0), List.of("序号", "工作事项", "具体目标", "对他人协助的需求"), true); + for (PersonalReportPlanItemRespVO item : safeList(items)) { + XWPFTableRow row = table.createRow(); + setCellText(row.getCell(0), formatNumber(item.getItemNumber()), false); + setCellText(row.getCell(1), item.getItemTitle(), false); + setCellText(row.getCell(2), buildStructuredPlanText(item, includeDetail), false); + setCellText(row.getCell(3), item.getSupportNeed(), false); + } + } + + private void addFeedbackTable(XWPFDocument document, MonthlyReportApprovalRecordRespVO record) { + XWPFTable table = document.createTable(1, 4); + setTableWidth(table); + setRowText(table.getRow(0), List.of("优势/劣势项", "优势/劣势描述", "行为事例", "改进提升建议"), true); + XWPFTableRow strengthRow = table.createRow(); + setRowText(strengthRow, values("优势", + record == null ? null : record.getStrengthDesc(), + record == null ? null : record.getStrengthExample(), + ""), false); + XWPFTableRow weaknessRow = table.createRow(); + setRowText(weaknessRow, values("劣势", + record == null ? null : record.getWeaknessDesc(), + record == null ? null : record.getWeaknessExample(), + record == null ? null : record.getImprovementSuggestion()), false); + } + + private void addProjectItemTable(XWPFDocument document, List items, boolean showHours) { + XWPFTable table = document.createTable(1, showHours ? 4 : 3); + setTableWidth(table); + setRowText(table.getRow(0), showHours + ? List.of("工作内容", "工时", "优先级", "进度") + : List.of("工作内容", "优先级", "进度"), true); + for (ProjectReportItemRespVO item : safeList(items)) { + XWPFTableRow row = table.createRow(); + setRowText(row, showHours + ? values(item.getItemTitle(), formatDecimal(item.getWorkHours()), item.getPriorityCode(), + formatProgress(item.getProgressRate())) + : values(item.getItemTitle(), item.getPriorityCode(), formatProgress(item.getProgressRate())), + false); + } + } + + private void addKeyValueTable(XWPFDocument document, List> rows) { + XWPFTable table = document.createTable(rows.size(), 2); + setTableWidth(table); + for (int i = 0; i < rows.size(); i++) { + setRowText(table.getRow(i), rows.get(i), i == 0 && rows.size() == 1); + } + } + + private void setTableWidth(XWPFTable table) { + table.setWidth("100%"); + } + + private void setRowText(XWPFTableRow row, List values, boolean bold) { + for (int i = 0; i < values.size(); i++) { + setCellText(row.getCell(i), values.get(i), bold); + } + } + + private void setCellText(XWPFTableCell cell, String value, boolean bold) { + while (cell.getParagraphs().size() > 0) { + cell.removeParagraph(0); + } + XWPFParagraph paragraph = cell.addParagraph(); + XWPFRun run = paragraph.createRun(); + run.setBold(bold); + String[] lines = text(value).split("\\R", -1); + for (int i = 0; i < lines.length; i++) { + if (i > 0) { + run.addBreak(); + } + run.setText(lines[i]); + } + } + + private void setCellTextByTemplateParagraph(XWPFTableCell cell, String value, boolean bold) { + unsetNoWrap(cell); + XWPFParagraph paragraph; + if (cell.getParagraphs() == null || cell.getParagraphs().isEmpty()) { + paragraph = cell.addParagraph(); + } else { + paragraph = cell.getParagraphs().get(0); + for (int i = cell.getParagraphs().size() - 1; i >= 1; i--) { + cell.removeParagraph(i); + } + } + List runs = paragraph.getRuns(); + XWPFRun firstRun; + if (runs == null || runs.isEmpty()) { + firstRun = paragraph.createRun(); + } else { + firstRun = runs.get(0); + for (int i = runs.size() - 1; i >= 1; i--) { + paragraph.removeRun(i); + } + } + firstRun.setBold(bold); + applyRunText(firstRun, value); + } + + private void unsetNoWrap(XWPFTableCell cell) { + if (cell == null || cell.getCTTc() == null) { + return; + } + var tcPr = cell.getCTTc().getTcPr(); + if (tcPr != null && tcPr.isSetNoWrap()) { + tcPr.unsetNoWrap(); + } + } + + private void setCellParagraphs(XWPFTableCell cell, List lines, boolean bold) { + while (cell.getParagraphs().size() > 0) { + cell.removeParagraph(0); + } + List safeLines = lines == null || lines.isEmpty() ? List.of("") : lines; + for (String line : safeLines) { + XWPFParagraph paragraph = cell.addParagraph(); + paragraph.setSpacingBefore(0); + paragraph.setSpacingAfter(0); + XWPFRun run = paragraph.createRun(); + run.setBold(bold); + run.setText(text(line)); + } + } + + private byte[] toBytes(XWPFDocument document) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + document.write(outputStream); + return outputStream.toByteArray(); + } + + private String buildTravelText(List travelSegments) { + if (travelSegments == null || travelSegments.isEmpty()) { + return ""; + } + return travelSegments.stream() + .map(item -> formatDate(item.getStartDate()) + "-" + formatDate(item.getEndDate()) + + "," + formatDecimal(item.getTravelDays()) + "天," + text(item.getLocation())) + .collect(java.util.stream.Collectors.joining("\n")); + } + + private String buildMemberText(List members) { + return safeList(members).stream() + .map(WorkReportMemberSnapshotRespVO::getUserName) + .filter(StringUtils::hasText) + .collect(java.util.stream.Collectors.joining("、")); + } + + private String appendHours(String title, BigDecimal hours) { + if (hours == null) { + return text(title); + } + return text(title) + "(" + formatDecimal(hours) + "h)"; + } + + private String formatPeriod(Object report) { + if (report instanceof WeeklyReportRespVO weeklyReport) { + return formatDate(weeklyReport.getPeriodStartDate()) + "-" + formatDate(weeklyReport.getPeriodEndDate()); + } + if (report instanceof MonthlyReportRespVO monthlyReport) { + return formatDate(monthlyReport.getPeriodStartDate()) + "-" + formatDate(monthlyReport.getPeriodEndDate()); + } + if (report instanceof ProjectReportRespVO projectReport) { + return formatDate(projectReport.getPeriodStartDate()) + "-" + formatDate(projectReport.getPeriodEndDate()); + } + return ""; + } + + private String formatDate(LocalDate value) { + return value == null ? "" : DATE_FORMATTER.format(value); + } + + private String formatDateTime(LocalDateTime value) { + return value == null ? "" : DATE_TIME_FORMATTER.format(value); + } + + private String formatNumber(Integer value) { + return value == null ? "" : String.valueOf(value); + } + + private String formatDecimal(BigDecimal value) { + return value == null ? "" : value.stripTrailingZeros().toPlainString(); + } + + private String formatProgress(BigDecimal value) { + return value == null ? "" : value.stripTrailingZeros().toPlainString() + "%"; + } + + private String buildFilename(String reportTypeName, String ownerName, String periodLabel) { + return sanitizeFilename(reportTypeName + "_" + text(ownerName) + "_" + text(periodLabel)) + ".docx"; + } + + private String sanitizeFilename(String filename) { + return filename.replaceAll("[\\\\/:*?\"<>|\\r\\n]+", "_"); + } + + private String text(String value) { + return StringUtils.hasText(value) ? value.trim() : ""; + } + + private String textOf(Object value) { + return value == null ? "" : text(String.valueOf(value)); + } + + private List row(String label, String value) { + return List.of(text(label), text(value)); + } + + private List values(String... values) { + List result = new ArrayList<>(values.length); + for (String value : values) { + result.add(text(value)); + } + return result; + } + + private List safeList(List source) { + return source == null ? List.of() : source; + } + + private record StructuredSectionData(String category, List tasks) { + } + + private record StructuredTaskData(String title, String detail, String priority, BigDecimal progress, BigDecimal hours) { + } + + private record StructuredMetricsData(String priority, BigDecimal progress, BigDecimal hours) { + } + + private record StructuredLineData(String category, StructuredTaskData task) { + } + + private record TaskDetailParts(String title, String detail) { + } + + private record DocumentContent(String filename, byte[] content) { + } +} \ No newline at end of file diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportExportFile.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportExportFile.java new file mode 100644 index 0000000..a0c1980 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportExportFile.java @@ -0,0 +1,4 @@ +package com.njcn.rdms.module.project.service.workreport.export; + +public record WorkReportExportFile(String filename, String contentType, byte[] content) { +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportService.java new file mode 100644 index 0000000..66c5550 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportService.java @@ -0,0 +1,45 @@ +package com.njcn.rdms.module.project.service.workreport.monthly; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportApproveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportExportVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; + +import java.util.List; + +public interface MonthlyReportService { + + MonthlyReportRespVO initMonthlyReport(); + + MonthlyReportRespVO previewMonthlyDefaultDraft(MonthlyReportDefaultDraftReqVO reqVO); + + Long createMonthlyReport(MonthlyReportSaveReqVO reqVO); + + void updateMonthlyReport(Long id, MonthlyReportSaveReqVO reqVO); + + MonthlyReportRespVO getMonthlyReport(Long id); + + PageResult getMonthlyReportPage(MonthlyReportPageReqVO reqVO); + + PageResult getMonthlyApprovalPage(MonthlyReportPageReqVO reqVO); + + void submitMonthlyReport(Long id); + + void approveMonthlyReport(Long id, MonthlyReportApproveReqVO reqVO); + + void rejectMonthlyReport(Long id, WorkReportStatusActionReqVO reqVO); + + void deleteMonthlyReport(Long id); + + List getMonthlyStatusLogs(Long id); + + List getMonthlyApprovalRecords(Long id); + + List getMonthlyExportList(MonthlyReportPageReqVO reqVO); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportServiceImpl.java new file mode 100644 index 0000000..d64168f --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportServiceImpl.java @@ -0,0 +1,97 @@ +package com.njcn.rdms.module.project.service.workreport.monthly; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportApproveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportExportVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; +import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService; +import com.njcn.rdms.module.project.service.workreport.defaultdraft.WorkReportDefaultDraftService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class MonthlyReportServiceImpl implements MonthlyReportService { + + @Resource + private WorkReportCommonService workReportCommonService; + @Resource + private WorkReportDefaultDraftService workReportDefaultDraftService; + + @Override + public MonthlyReportRespVO initMonthlyReport() { + return workReportCommonService.initMonthlyReport(); + } + + @Override + public MonthlyReportRespVO previewMonthlyDefaultDraft(MonthlyReportDefaultDraftReqVO reqVO) { + return workReportDefaultDraftService.previewMonthlyDefaultDraft(reqVO); + } + + @Override + public Long createMonthlyReport(MonthlyReportSaveReqVO reqVO) { + return workReportCommonService.createMonthlyReport(reqVO); + } + + @Override + public void updateMonthlyReport(Long id, MonthlyReportSaveReqVO reqVO) { + workReportCommonService.updateMonthlyReport(id, reqVO); + } + + @Override + public MonthlyReportRespVO getMonthlyReport(Long id) { + return workReportCommonService.getMonthlyReport(id); + } + + @Override + public PageResult getMonthlyReportPage(MonthlyReportPageReqVO reqVO) { + return workReportCommonService.getMonthlyReportPage(reqVO); + } + + @Override + public PageResult getMonthlyApprovalPage(MonthlyReportPageReqVO reqVO) { + return workReportCommonService.getMonthlyApprovalPage(reqVO); + } + + @Override + public void submitMonthlyReport(Long id) { + workReportCommonService.submitMonthlyReport(id); + } + + @Override + public void approveMonthlyReport(Long id, MonthlyReportApproveReqVO reqVO) { + workReportCommonService.approveMonthlyReport(id, reqVO); + } + + @Override + public void rejectMonthlyReport(Long id, WorkReportStatusActionReqVO reqVO) { + workReportCommonService.rejectMonthlyReport(id, reqVO); + } + + @Override + public void deleteMonthlyReport(Long id) { + workReportCommonService.deleteMonthlyReport(id); + } + + @Override + public List getMonthlyStatusLogs(Long id) { + return workReportCommonService.getMonthlyStatusLogs(id); + } + + @Override + public List getMonthlyApprovalRecords(Long id) { + return workReportCommonService.getMonthlyApprovalRecords(id); + } + + @Override + public List getMonthlyExportList(MonthlyReportPageReqVO reqVO) { + return workReportCommonService.getMonthlyExportList(reqVO); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportService.java new file mode 100644 index 0000000..8a2809c --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportService.java @@ -0,0 +1,47 @@ +package com.njcn.rdms.module.project.service.workreport.project; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportExportVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportOwnerProjectOptionRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; + +import java.util.List; + +public interface ProjectReportService { + + List getOwnerProjectOptions(); + + ProjectReportRespVO initProjectReport(Long projectId); + + ProjectReportRespVO previewProjectDefaultDraft(Long projectId, ProjectReportDefaultDraftReqVO reqVO); + + Long createProjectReport(ProjectReportSaveReqVO reqVO); + + void updateProjectReport(Long id, ProjectReportSaveReqVO reqVO); + + ProjectReportRespVO getProjectReport(Long id); + + PageResult getProjectReportPage(ProjectReportPageReqVO reqVO); + + PageResult getProjectApprovalPage(ProjectReportPageReqVO reqVO); + + void submitProjectReport(Long id); + + void approveProjectReport(Long id, WorkReportStatusActionReqVO reqVO); + + void rejectProjectReport(Long id, WorkReportStatusActionReqVO reqVO); + + void deleteProjectReport(Long id); + + List getProjectStatusLogs(Long id); + + List getProjectApprovalRecords(Long id); + + List getProjectExportList(ProjectReportPageReqVO reqVO); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportServiceImpl.java new file mode 100644 index 0000000..0e87368 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportServiceImpl.java @@ -0,0 +1,106 @@ +package com.njcn.rdms.module.project.service.workreport.project; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportExportVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportOwnerProjectOptionRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; +import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService; +import com.njcn.rdms.module.project.service.workreport.defaultdraft.WorkReportDefaultDraftService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ProjectReportServiceImpl implements ProjectReportService { + + @Resource + private WorkReportCommonService workReportCommonService; + @Resource + private WorkReportDefaultDraftService workReportDefaultDraftService; + + @Override + public List getOwnerProjectOptions() { + return workReportCommonService.getProjectReportOwnerProjectOptions(); + } + + @Override + public ProjectReportRespVO initProjectReport(Long projectId) { + workReportCommonService.validateCurrentUserIsProjectReportProjectOwner(projectId); + return workReportCommonService.initProjectReport(projectId); + } + + @Override + public ProjectReportRespVO previewProjectDefaultDraft(Long projectId, ProjectReportDefaultDraftReqVO reqVO) { + workReportCommonService.validateCurrentUserIsProjectReportProjectOwner(projectId); + return workReportDefaultDraftService.previewProjectDefaultDraft(projectId, reqVO); + } + + @Override + public Long createProjectReport(ProjectReportSaveReqVO reqVO) { + workReportCommonService.validateCurrentUserIsProjectReportProjectOwner(reqVO.getProjectId()); + return workReportCommonService.createProjectReport(reqVO); + } + + @Override + public void updateProjectReport(Long id, ProjectReportSaveReqVO reqVO) { + workReportCommonService.validateCurrentUserIsProjectReportProjectOwner(reqVO.getProjectId()); + workReportCommonService.updateProjectReport(id, reqVO); + } + + @Override + public ProjectReportRespVO getProjectReport(Long id) { + return workReportCommonService.getProjectReport(id); + } + + @Override + public PageResult getProjectReportPage(ProjectReportPageReqVO reqVO) { + return workReportCommonService.getProjectReportPage(reqVO); + } + + @Override + public PageResult getProjectApprovalPage(ProjectReportPageReqVO reqVO) { + return workReportCommonService.getProjectApprovalPage(reqVO); + } + + @Override + public void submitProjectReport(Long id) { + workReportCommonService.submitProjectReport(id); + } + + @Override + public void approveProjectReport(Long id, WorkReportStatusActionReqVO reqVO) { + workReportCommonService.approveProjectReport(id, reqVO); + } + + @Override + public void rejectProjectReport(Long id, WorkReportStatusActionReqVO reqVO) { + workReportCommonService.rejectProjectReport(id, reqVO); + } + + @Override + public void deleteProjectReport(Long id) { + workReportCommonService.deleteProjectReport(id); + } + + @Override + public List getProjectStatusLogs(Long id) { + return workReportCommonService.getProjectStatusLogs(id); + } + + @Override + public List getProjectApprovalRecords(Long id) { + return workReportCommonService.getProjectApprovalRecords(id); + } + + @Override + public List getProjectExportList(ProjectReportPageReqVO reqVO) { + return workReportCommonService.getProjectExportList(reqVO); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportService.java new file mode 100644 index 0000000..da28fe6 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportService.java @@ -0,0 +1,44 @@ +package com.njcn.rdms.module.project.service.workreport.weekly; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportExportVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; + +import java.util.List; + +public interface WeeklyReportService { + + WeeklyReportRespVO initWeeklyReport(); + + WeeklyReportRespVO previewWeeklyDefaultDraft(WeeklyReportDefaultDraftReqVO reqVO); + + Long createWeeklyReport(WeeklyReportSaveReqVO reqVO); + + void updateWeeklyReport(Long id, WeeklyReportSaveReqVO reqVO); + + WeeklyReportRespVO getWeeklyReport(Long id); + + PageResult getWeeklyReportPage(WeeklyReportPageReqVO reqVO); + + PageResult getWeeklyApprovalPage(WeeklyReportPageReqVO reqVO); + + void submitWeeklyReport(Long id); + + void approveWeeklyReport(Long id, WorkReportStatusActionReqVO reqVO); + + void rejectWeeklyReport(Long id, WorkReportStatusActionReqVO reqVO); + + void deleteWeeklyReport(Long id); + + List getWeeklyStatusLogs(Long id); + + List getWeeklyApprovalRecords(Long id); + + List getWeeklyExportList(WeeklyReportPageReqVO reqVO); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportServiceImpl.java new file mode 100644 index 0000000..4e7ce87 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportServiceImpl.java @@ -0,0 +1,96 @@ +package com.njcn.rdms.module.project.service.workreport.weekly; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportExportVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportDefaultDraftReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; +import com.njcn.rdms.module.project.service.workreport.defaultdraft.WorkReportDefaultDraftService; +import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class WeeklyReportServiceImpl implements WeeklyReportService { + + @Resource + private WorkReportCommonService workReportCommonService; + @Resource + private WorkReportDefaultDraftService workReportDefaultDraftService; + + @Override + public WeeklyReportRespVO initWeeklyReport() { + return workReportCommonService.initWeeklyReport(); + } + + @Override + public WeeklyReportRespVO previewWeeklyDefaultDraft(WeeklyReportDefaultDraftReqVO reqVO) { + return workReportDefaultDraftService.previewWeeklyDefaultDraft(reqVO); + } + + @Override + public Long createWeeklyReport(WeeklyReportSaveReqVO reqVO) { + return workReportCommonService.createWeeklyReport(reqVO); + } + + @Override + public void updateWeeklyReport(Long id, WeeklyReportSaveReqVO reqVO) { + workReportCommonService.updateWeeklyReport(id, reqVO); + } + + @Override + public WeeklyReportRespVO getWeeklyReport(Long id) { + return workReportCommonService.getWeeklyReport(id); + } + + @Override + public PageResult getWeeklyReportPage(WeeklyReportPageReqVO reqVO) { + return workReportCommonService.getWeeklyReportPage(reqVO); + } + + @Override + public PageResult getWeeklyApprovalPage(WeeklyReportPageReqVO reqVO) { + return workReportCommonService.getWeeklyApprovalPage(reqVO); + } + + @Override + public void submitWeeklyReport(Long id) { + workReportCommonService.submitWeeklyReport(id); + } + + @Override + public void approveWeeklyReport(Long id, WorkReportStatusActionReqVO reqVO) { + workReportCommonService.approveWeeklyReport(id, reqVO); + } + + @Override + public void rejectWeeklyReport(Long id, WorkReportStatusActionReqVO reqVO) { + workReportCommonService.rejectWeeklyReport(id, reqVO); + } + + @Override + public void deleteWeeklyReport(Long id) { + workReportCommonService.deleteWeeklyReport(id); + } + + @Override + public List getWeeklyStatusLogs(Long id) { + return workReportCommonService.getWeeklyStatusLogs(id); + } + + @Override + public List getWeeklyApprovalRecords(Long id) { + return workReportCommonService.getWeeklyApprovalRecords(id); + } + + @Override + public List getWeeklyExportList(WeeklyReportPageReqVO reqVO) { + return workReportCommonService.getWeeklyExportList(reqVO); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/monthly-report-template.docx b/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/monthly-report-template.docx new file mode 100644 index 0000000000000000000000000000000000000000..e6e8834a0916914f252ab295a6d44c76e2740fb8 GIT binary patch literal 25281 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 literal 0 HcmV?d00001 diff --git a/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/project-report-template.docx b/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/project-report-template.docx new file mode 100644 index 0000000000000000000000000000000000000000..503aea2d340a65d8a8f1fd9a0164d7e36a0f9529 GIT binary patch literal 15071 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 literal 0 HcmV?d00001 diff --git a/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/weekly-report-template.docx b/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/weekly-report-template.docx new file mode 100644 index 0000000000000000000000000000000000000000..97bb06c38e114f8d9ca63640ae2f115f564a60d4 GIT binary patch literal 24346 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 literal 0 HcmV?d00001