# ScheduleVsActualController.java

    package cloud.wconnect.workperformance.controller.sva;

    import cloud.wconnect.commonlib.annotation.ApiId;
    import cloud.wconnect.commonlib.component.SessionContext;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.exception.InternalServerErrorException;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.I18nMessageUtils;
    import cloud.wconnect.workperformance.entity.sva.SvaComment;
    import cloud.wconnect.workperformance.entity.sva.employees.AttendanceEmployee;
    import cloud.wconnect.workperformance.entity.sva.setting.DepartmentCategory;
    import cloud.wconnect.workperformance.pojo.dto.sva.schedulevsactual.ScheduleVsActualDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.schedulevsactual.SvaCommentDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.schedulevsactual.SvaCommentListDTO;
    import cloud.wconnect.workperformance.pojo.query.sva.schedulevsactual.ScheduleVsActualQuery;
    import cloud.wconnect.workperformance.pojo.query.sva.schedulevsactual.SvaCommentQuery;
    import cloud.wconnect.workperformance.service.sva.employees.IAttendanceEmployeeService;
    import cloud.wconnect.workperformance.service.sva.scheduleactual.IScheduleVsActualService;
    import cloud.wconnect.workperformance.service.sva.scheduleactual.ISvaCommentService;
    import cloud.wconnect.workperformance.service.sva.setting.IDepartmentCategoryService;
    import com.google.common.collect.ImmutableMap;
    import jakarta.annotation.Resource;
    import jakarta.servlet.http.HttpServletResponse;
    import jakarta.validation.Valid;
    import org.jxls.transform.poi.JxlsPoiTemplateFillerBuilder;
    import org.springframework.context.i18n.LocaleContextHolder;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.web.bind.annotation.*;

    import java.io.IOException;
    import java.io.InputStream;
    import java.util.*;

    /**
     * <p>
     * Schedule vs actual 前端控制器
     * </p>
     *
     * @author zc
     * @since 2023-06-22
     */
    @RestController
    @RequestMapping("/sva/schedule-actual")
    public class ScheduleVsActualController {
        @Resource
        private IScheduleVsActualService scheduleVsActualService;
        @Resource
        private IDepartmentCategoryService departmentCategoryService;
        @Resource
        private ISvaCommentService svaCommentService;
        @Resource
        private IAttendanceEmployeeService employeeService;
        @Resource
        private PropertyI18nRedisService propertyI18nRedisService;
        @Resource
        private SessionContext sessionContext;
        @Resource
        private I18nMessageUtils i18nMessageUtils;

        /**
         * 获取指定日期范围内 Schedule vs actual 数据
         *
         * @param property 资产标识
         * @param query    查询条件
         * @return ScheduleVsActualDTO
         * @author zc
         * @date 2023/6/22
         */
        @ApiId(377)
        @GetMapping
        public ScheduleVsActualDTO getScheduleVsActual(@RequestParam String property, @Valid ScheduleVsActualQuery query) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            // 校验查询参数
            checkParam(query.getFromDate(), query.getToDate(), propertyDTO.getTimezone());
            return scheduleVsActualService.getScheduleVsActual(propertyDTO, query, sessionContext.getLoginUserId());
        }

        /**
         * 导出指定日期范围内 Schedule vs Actual 内容
         *
         * @param property 资产标识
         * @param query    查询条件
         * @param response response
         * @author zc
         * @date 2023/6/29
         */
        @ApiId(378)
        @GetMapping("/report")
        public void export(@RequestParam String property, @Valid ScheduleVsActualQuery query, HttpServletResponse response) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            // 校验查询参数
            checkParam(query.getFromDate(), query.getToDate(), propertyDTO.getTimezone());

            try (InputStream is = new ClassPathResource("tmpl-xls/schedule_vs_actual_tmpl_" + LocaleContextHolder.getLocale() + ".xlsx").getInputStream()) {
                HashMap<String, Object> data = scheduleVsActualService.generateContext(propertyDTO, query, sessionContext.getLoginUserId());

                String fileName = "ScheduleVsActual.xlsx";
                response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
                response.setHeader("Content-Disposition", String.join("", "attachment; filename=", fileName));

                JxlsPoiTemplateFillerBuilder.newInstance()
                        .withTemplate(is)
                        .buildAndFill(data, response::getOutputStream);
            } catch (IOException exp) {
                throw new InternalServerErrorException(BaseErrorCodeEnum.SYSTEM_ERROR.getI18nCode(),
                        BaseErrorCodeEnum.SYSTEM_ERROR.getCode(),
                        "Export schedule vs actual failed; exp:" + exp);
            }
        }

        /**
         * 获取指定日期的评论集合
         *
         * @param property 资产标识
         * @param query    查询条件
         * @return SvaCommentDTO 集合
         * @author zc
         * @date 2025/2/7
         */
        @ApiId(382)
        @GetMapping("/comment")
        public List<SvaCommentListDTO> listComment(@RequestParam String property, @Valid SvaCommentQuery query) {
            int tenantId = sessionContext.getTenantSymbolBO().getId();
            int propertyId = sessionContext.getPropertyId(property);

            // 获取评论数据
            return svaCommentService.listComment(tenantId, propertyId, query);
        }

        /**
         * 添加评论
         *
         * @param property   资产标识
         * @param commentDTO 评论内容
         * @return empty map
         * @author zc
         * @date 2023/6/28
         */
        @ApiId(383)
        @PostMapping("/comment")
        public Map<String, Integer> addComment(@RequestParam String property, @Valid @RequestBody SvaCommentDTO commentDTO) {
            int propertyId = sessionContext.getPropertyId(property);

            // 校验 positionId
            AttendanceEmployee employee = employeeService.getByPositionId(propertyId, commentDTO.getPositionId());
            if (Objects.isNull(employee)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "Cannot find the employee by this positionId: " + commentDTO.getPositionId());
            }
            // 校验部门分类是否存在
            DepartmentCategory departmentCategory = departmentCategoryService.checkExist(commentDTO.getDepartmentCategoryId());
            // 校验数据归属
            departmentCategoryService.checkBelong(propertyId, departmentCategory);

            // 保存数据并返回 id
            return ImmutableMap.of("id", svaCommentService.save(propertyId, commentDTO, sessionContext.getLoginUserId()));
        }

        /**
         * 删除评论
         *
         * @param property 资产标识
         * @param cid      评论 id
         * @return empty map
         * @author zc
         * @date 2025/12/18
         */
        @ApiId(384)
        @DeleteMapping("/comment/{cid:\\d+}")
        public Map<Object, Object> deleteComment(@RequestParam String property, @PathVariable int cid) {
            int propertyId = sessionContext.getPropertyId(property);
            int loginUserId = sessionContext.getLoginUserId();
            // 校验数据是否存在
            SvaComment comment = svaCommentService.checkExist(cid);
            // 校验数据归属
            svaCommentService.checkBelong(propertyId, loginUserId, comment);
            // 删除数据
            svaCommentService.removeById(cid);

            return Collections.emptyMap();
        }

        /**
         * 校验参数
         *
         * @param fromDate 开始时间
         * @param toDate   结束时间
         * @param timezone 时区
         * @author zc
         * @date 2023/6/22
         */
        private void checkParam(long fromDate, long toDate, String timezone) {
            // 开始时间不能大于结束时间
            if (fromDate > toDate) {
                throw new BadRequestException(CommonErrorI18nConstant.START_DATE_CANNOT_GREATER_THAN_END_DATE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The fromDate cannot be greater than the toDate");
            }

            // 例如：筛选天数为 7 天，from date 与 to date 差值为 6
            int difference = 6;
            if (DateTimeUtils.numberOfDaysApart(fromDate, toDate, timezone) != difference) {
                throw new BadRequestException(CommonErrorI18nConstant.SELECT_DATE_LIMIT,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate(7),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The number of screening days exceeds the range of 7 days");
            }
        }
    }