package com.kaidi.oa.web;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kaidi.oa.common.ApiException;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.NotFoundException;
import com.kaidi.oa.domain.DeclAcceptance;
import com.kaidi.oa.repository.DeclAcceptanceRepository;
import com.kaidi.oa.repository.DeclarationRepository;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 申报项目验收任务(创新研发中心·申报服务部,需求 §3 验收管理)。
*
* 区别于工程监理部竣工验收(CompletionAcceptance 绑 SupervisionProject),本控制器面向「科技计划/技改」等
* 政府申报项目的结题验收,补 PARTIAL(med):自动生成验收任务 + 验收材料编制审核状态机。
*
* - 新建即按 acceptType 自动生成标准验收材料清单(总结报告/财务决算/检测报告/用户意见…)置「待准备」;
* - 材料编制:/{id}/materials 勾选材料齐套(ready),齐套后可提交审核;
* - 状态机:待准备 → 材料编制中 → 待审核 → 已审核 → 已验收;审核 /{id}/review 通过进「已审核」、
* 驳回退回「材料编制中」;/{id}/accept 完成验收置「已验收」回写实际验收日;
* - 到期分级预警(/alerts/due):未验收且 planDate 临近/逾期的验收任务。
*
* 日期 String(YYYY-MM-DD);状态迁移/批量改写带 {@link Transactional}。
*/
@RestController
@RequestMapping("/api/oa/decl-acceptances")
public class DeclAcceptanceController {
private static final String ST_PREP = "待准备";
private static final String ST_DRAFTING = "材料编制中";
private static final String ST_TOREVIEW = "待审核";
private static final String ST_REVIEWED = "已审核";
private static final String ST_ACCEPTED = "已验收";
private final DeclAcceptanceRepository repo;
private final DeclarationRepository declRepo;
private final CurrentUserResolver currentUser;
private final ObjectMapper objectMapper;
public DeclAcceptanceController(DeclAcceptanceRepository repo,
DeclarationRepository declRepo,
CurrentUserResolver currentUser,
ObjectMapper objectMapper) {
this.repo = repo;
this.declRepo = declRepo;
this.currentUser = currentUser;
this.objectMapper = objectMapper;
}
@GetMapping
public ApiResp> list(@RequestParam(required = false) String status,
@RequestParam(required = false) Long declarationId) {
if (status != null && !status.isBlank()) {
return ApiResp.ok(repo.findByStatus(status));
}
if (declarationId != null) {
return ApiResp.ok(repo.findByDeclarationId(declarationId));
}
return ApiResp.ok(repo.findAll());
}
@GetMapping("/{id}")
public ApiResp get(@PathVariable Long id) {
return ApiResp.ok(load(id));
}
public record CreateRequest(Long declarationId, String projectName, String authority,
String acceptType, String planDate, String owner, String remark) {
}
/** 自动生成验收任务:按 acceptType 内置标准验收材料清单(系统自动生成),初始「待准备」。 */
@PostMapping
@Transactional
public ApiResp create(@RequestBody CreateRequest req, HttpServletRequest request) {
if (req.projectName() == null || req.projectName().isBlank()) {
throw new ApiException(400, "验收项目名称(projectName) 不能为空");
}
if (req.declarationId() != null && !declRepo.existsById(req.declarationId())) {
throw new ApiException(400, "关联的申报项目不存在:" + req.declarationId());
}
DeclAcceptance a = new DeclAcceptance();
a.setCode("YS-" + (repo.count() + 1));
a.setDeclarationId(req.declarationId());
a.setProjectName(req.projectName().trim());
a.setAuthority(req.authority());
String type = req.acceptType() == null || req.acceptType().isBlank() ? "结题验收" : req.acceptType();
a.setAcceptType(type);
a.setStatus(ST_PREP);
a.setMaterialsJson(writeMaterials(defaultMaterials(type)));
a.setPlanDate(req.planDate());
a.setOwner(req.owner() == null || req.owner().isBlank() ? currentUser.resolveLabel(request) : req.owner());
a.setRemark(req.remark());
a.setCreatedAt(Instant.now());
return ApiResp.ok(repo.save(a));
}
@PatchMapping("/{id}")
@Transactional
public ApiResp update(@PathVariable Long id, @RequestBody CreateRequest req) {
DeclAcceptance a = load(id);
if (req.projectName() != null && !req.projectName().isBlank()) a.setProjectName(req.projectName().trim());
if (req.authority() != null) a.setAuthority(req.authority());
if (req.acceptType() != null && !req.acceptType().isBlank()) a.setAcceptType(req.acceptType());
if (req.planDate() != null) a.setPlanDate(req.planDate());
if (req.owner() != null) a.setOwner(req.owner());
if (req.remark() != null) a.setRemark(req.remark());
// status / materialsJson / acceptedDate 仅由业务动作推进,禁止客户端直写。
return ApiResp.ok(repo.save(a));
}
@DeleteMapping("/{id}")
public ApiResp delete(@PathVariable Long id) {
if (!repo.existsById(id)) {
throw new NotFoundException("decl acceptance not found: " + id);
}
repo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 材料编制 ----------
public record MaterialFlag(String name, Boolean ready) {
}
public record MaterialsRequest(List materials) {
}
/** 勾选/更新验收材料齐套状态。首次编制把「待准备」推进到「材料编制中」。 */
@PostMapping("/{id}/materials")
@Transactional
public ApiResp materials(@PathVariable Long id, @RequestBody MaterialsRequest req) {
DeclAcceptance a = load(id);
if (ST_ACCEPTED.equals(a.getStatus())) {
throw new ApiException(409, "已验收,材料不可再改");
}
List