|
@@ -61,11 +61,14 @@ public class WorkHoursCalcService {
|
|
|
// prd 应填报人显示名称文本,用于报表按姓名筛选;姓名取自人员档案成员组件显示值
|
|
// prd 应填报人显示名称文本,用于报表按姓名筛选;姓名取自人员档案成员组件显示值
|
|
|
private static final String REQUIRED_HOURS_EMPLOYEE_NAME = "textField_ms4bzjkm";
|
|
private static final String REQUIRED_HOURS_EMPLOYEE_NAME = "textField_ms4bzjkm";
|
|
|
|
|
|
|
|
- // prd 外部员工关联项目经理:项目档案成员子表字段
|
|
|
|
|
|
|
+ // prd 全员项目参与关系 + 外部员工项目经理:项目档案字段
|
|
|
private static final String PROJECT_SUB_TABLE = "tableField_mkowyn6d"; // 成员子表(人员明细)
|
|
private static final String PROJECT_SUB_TABLE = "tableField_mkowyn6d"; // 成员子表(人员明细)
|
|
|
private static final String PROJECT_SUB_MEMBER = "employeeField_mmbfe0ij"; // 子表-员工(成员)
|
|
private static final String PROJECT_SUB_MEMBER = "employeeField_mmbfe0ij"; // 子表-员工(成员)
|
|
|
private static final String PROJECT_SUB_PM = "employeeField_mkoxpswf"; // 子表-项目经理
|
|
private static final String PROJECT_SUB_PM = "employeeField_mkoxpswf"; // 子表-项目经理
|
|
|
private static final String PROJECT_SUB_OFFLINE = "dateField_mo6s11tc"; // 子表-下线时间
|
|
private static final String PROJECT_SUB_OFFLINE = "dateField_mo6s11tc"; // 子表-下线时间
|
|
|
|
|
+ // prd 仅启用项目参与人员需要统计应填报工时
|
|
|
|
|
+ private static final String PROJECT_STATUS = "textField_mpwc5r0q"; // 主表-项目状态
|
|
|
|
|
+ private static final String PROJECT_STATUS_ACTIVE = "启用";
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 主入口:计算指定月份每个工作日的应填报工时并写入宜搭(按天维度,每条记录8h)
|
|
* 主入口:计算指定月份每个工作日的应填报工时并写入宜搭(按天维度,每条记录8h)
|
|
@@ -93,10 +96,11 @@ public class WorkHoursCalcService {
|
|
|
return stats;
|
|
return stats;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 预取 Manager 数据: 内部员工=钉钉直属主管, 外部员工=项目 assignments (按天算 PM 在 concurrentUpsert 内层)
|
|
|
|
|
|
|
+ // 2. 预取 Manager + 启用项目参与关系;所有员工均须命中当天参与项目
|
|
|
ManagerData managerData = queryManagerData(personnelMap);
|
|
ManagerData managerData = queryManagerData(personnelMap);
|
|
|
stats.put("internalMgrCount", managerData.internal.size());
|
|
stats.put("internalMgrCount", managerData.internal.size());
|
|
|
stats.put("externalWithProjects", managerData.external.size());
|
|
stats.put("externalWithProjects", managerData.external.size());
|
|
|
|
|
+ stats.put("projectParticipantCount", managerData.projects.size());
|
|
|
|
|
|
|
|
// 3. 查询节假日规则
|
|
// 3. 查询节假日规则
|
|
|
Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(year));
|
|
Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(year));
|
|
@@ -144,7 +148,7 @@ public class WorkHoursCalcService {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 预取 Manager (内部=钉钉主管, 外部=项目 assignments)
|
|
|
|
|
|
|
+ // 2. 预取 Manager + 全员启用项目 assignments
|
|
|
ManagerData managerData = queryManagerData(personnelMap);
|
|
ManagerData managerData = queryManagerData(personnelMap);
|
|
|
|
|
|
|
|
// 3~4. 节假日 + 收窄到窗口内工作日 (跨月边界: 若窗口跨 6-30/7-1, 分别按各自月份的节假日规则)
|
|
// 3~4. 节假日 + 收窄到窗口内工作日 (跨月边界: 若窗口跨 6-30/7-1, 分别按各自月份的节假日规则)
|
|
@@ -235,7 +239,15 @@ public class WorkHoursCalcService {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. Manager 取值: 内部=[钉钉直属主管], 外部=当天参与项目的 PM 合并
|
|
|
|
|
|
|
+ // 2. 所有员工都必须参与启用项目;内部取直属主管,外部取当天参与项目的 PM 合并
|
|
|
|
|
+ Map<String, List<Assignment>> projectMap = queryProjectAssignments(Collections.singleton(userId));
|
|
|
|
|
+ List<Assignment> assignments = projectMap.get(userId);
|
|
|
|
|
+ if (!hasActiveProject(assignments, workDay)) {
|
|
|
|
|
+ result.put("success", false);
|
|
|
|
|
+ result.put("error", "员工当日未参与启用项目, 按业务规则跳过写入");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
boolean isInternal = "内部".equals(String.valueOf(info.get("radioField_mkow4ydo")));
|
|
boolean isInternal = "内部".equals(String.valueOf(info.get("radioField_mkow4ydo")));
|
|
|
List<String> managerIds;
|
|
List<String> managerIds;
|
|
|
if (isInternal) {
|
|
if (isInternal) {
|
|
@@ -255,8 +267,7 @@ public class WorkHoursCalcService {
|
|
|
managerIds = mgrId == null ? Collections.emptyList() : Collections.singletonList(mgrId);
|
|
managerIds = mgrId == null ? Collections.emptyList() : Collections.singletonList(mgrId);
|
|
|
} else {
|
|
} else {
|
|
|
// 外部员工: 按 workDay 算当天活跃项目 PM; 全空则按业务规则不写入
|
|
// 外部员工: 按 workDay 算当天活跃项目 PM; 全空则按业务规则不写入
|
|
|
- Map<String, List<Assignment>> extMap = queryProjectAssignments(Collections.singleton(userId));
|
|
|
|
|
- managerIds = computeDailyPms(extMap.get(userId), workDay);
|
|
|
|
|
|
|
+ managerIds = computeDailyPms(assignments, workDay);
|
|
|
if (managerIds.isEmpty()) {
|
|
if (managerIds.isEmpty()) {
|
|
|
result.put("success", false);
|
|
result.put("success", false);
|
|
|
result.put("error", "外部员工当日无可用项目 PM (无项目 / 项目已下线 / PM 为空), 按业务规则跳过写入");
|
|
result.put("error", "外部员工当日无可用项目 PM (无项目 / 项目已下线 / PM 为空), 按业务规则跳过写入");
|
|
@@ -328,6 +339,7 @@ public class WorkHoursCalcService {
|
|
|
ManagerData managerData = queryManagerData(subset);
|
|
ManagerData managerData = queryManagerData(subset);
|
|
|
stats.put("internalMgrCount", managerData.internal.size());
|
|
stats.put("internalMgrCount", managerData.internal.size());
|
|
|
stats.put("externalWithProjects", managerData.external.size());
|
|
stats.put("externalWithProjects", managerData.external.size());
|
|
|
|
|
+ stats.put("projectParticipantCount", managerData.projects.size());
|
|
|
|
|
|
|
|
// 4. 节假日 + 工作日
|
|
// 4. 节假日 + 工作日
|
|
|
Map<LocalDate, String> rules = queryHolidayRules(String.valueOf(year));
|
|
Map<LocalDate, String> rules = queryHolidayRules(String.valueOf(year));
|
|
@@ -839,6 +851,7 @@ public class WorkHoursCalcService {
|
|
|
AtomicInteger skippedExisting = new AtomicInteger(0);
|
|
AtomicInteger skippedExisting = new AtomicInteger(0);
|
|
|
AtomicInteger skippedOffline = new AtomicInteger(0);
|
|
AtomicInteger skippedOffline = new AtomicInteger(0);
|
|
|
AtomicInteger skippedBeforeHired = new AtomicInteger(0);
|
|
AtomicInteger skippedBeforeHired = new AtomicInteger(0);
|
|
|
|
|
+ AtomicInteger skippedNoProject = new AtomicInteger(0);
|
|
|
AtomicInteger skippedNoPm = new AtomicInteger(0);
|
|
AtomicInteger skippedNoPm = new AtomicInteger(0);
|
|
|
AtomicInteger skippedInconsistent = new AtomicInteger(0);
|
|
AtomicInteger skippedInconsistent = new AtomicInteger(0);
|
|
|
for (Map.Entry<String, Map<String, Object>> entry : personnelMap.entrySet()) {
|
|
for (Map.Entry<String, Map<String, Object>> entry : personnelMap.entrySet()) {
|
|
@@ -847,7 +860,7 @@ public class WorkHoursCalcService {
|
|
|
boolean isInternal = "内部".equals(String.valueOf(info.get("radioField_mkow4ydo")));
|
|
boolean isInternal = "内部".equals(String.valueOf(info.get("radioField_mkow4ydo")));
|
|
|
// 内部: 单值直属主管; 外部: assignments 交给内层按 workDay 算 PM
|
|
// 内部: 单值直属主管; 外部: assignments 交给内层按 workDay 算 PM
|
|
|
String internalMgrId = isInternal ? managerData.internal.get(empId) : null;
|
|
String internalMgrId = isInternal ? managerData.internal.get(empId) : null;
|
|
|
- List<Assignment> externalAssignments = isInternal ? null : managerData.external.get(empId);
|
|
|
|
|
|
|
+ List<Assignment> projectAssignments = managerData.projects.get(empId);
|
|
|
// prd 离职时间: 若有离职时间, 该日之后的应报工时不再生成
|
|
// prd 离职时间: 若有离职时间, 该日之后的应报工时不再生成
|
|
|
LocalDate offlineDate = parseToLocalDate(info.get(PERSONNEL_OFFLINE_DATE));
|
|
LocalDate offlineDate = parseToLocalDate(info.get(PERSONNEL_OFFLINE_DATE));
|
|
|
// prd 入职时间: 入职日前不生成, 入职当天保留
|
|
// prd 入职时间: 入职日前不生成, 入职当天保留
|
|
@@ -881,6 +894,11 @@ public class WorkHoursCalcService {
|
|
|
skippedBeforeHired.incrementAndGet();
|
|
skippedBeforeHired.incrementAndGet();
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
+ // prd 所有员工均须在启用项目中且当日未下线;无项目不统计应填报工时
|
|
|
|
|
+ if (!hasActiveProject(projectAssignments, workDay)) {
|
|
|
|
|
+ skippedNoProject.incrementAndGet();
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
// prd 外部员工: 当天参与项目的 PM 集合为空(无项目/PM 全空/项目全下线) → 不生成记录
|
|
// prd 外部员工: 当天参与项目的 PM 集合为空(无项目/PM 全空/项目全下线) → 不生成记录
|
|
|
List<String> managerIds;
|
|
List<String> managerIds;
|
|
|
if (isInternal) {
|
|
if (isInternal) {
|
|
@@ -888,7 +906,7 @@ public class WorkHoursCalcService {
|
|
|
? Collections.emptyList()
|
|
? Collections.emptyList()
|
|
|
: Collections.singletonList(internalMgrId);
|
|
: Collections.singletonList(internalMgrId);
|
|
|
} else {
|
|
} else {
|
|
|
- managerIds = computeDailyPms(externalAssignments, workDay);
|
|
|
|
|
|
|
+ managerIds = computeDailyPms(projectAssignments, workDay);
|
|
|
if (managerIds.isEmpty()) {
|
|
if (managerIds.isEmpty()) {
|
|
|
skippedNoPm.incrementAndGet();
|
|
skippedNoPm.incrementAndGet();
|
|
|
continue;
|
|
continue;
|
|
@@ -943,6 +961,9 @@ public class WorkHoursCalcService {
|
|
|
if (skippedBeforeHired.get() > 0) {
|
|
if (skippedBeforeHired.get() > 0) {
|
|
|
log.info("入职日期过滤: 跳过{}条 workDay < hiredDate 的记录", skippedBeforeHired.get());
|
|
log.info("入职日期过滤: 跳过{}条 workDay < hiredDate 的记录", skippedBeforeHired.get());
|
|
|
}
|
|
}
|
|
|
|
|
+ if (skippedNoProject.get() > 0) {
|
|
|
|
|
+ log.info("启用项目参与过滤: 跳过{}条 当天未参与启用项目的记录", skippedNoProject.get());
|
|
|
|
|
+ }
|
|
|
if (skippedNoPm.get() > 0) {
|
|
if (skippedNoPm.get() > 0) {
|
|
|
log.info("外部员工无 PM 过滤: 跳过{}条 当天无活跃项目 PM 的记录", skippedNoPm.get());
|
|
log.info("外部员工无 PM 过滤: 跳过{}条 当天无活跃项目 PM 的记录", skippedNoPm.get());
|
|
|
}
|
|
}
|
|
@@ -1676,11 +1697,12 @@ public class WorkHoursCalcService {
|
|
|
/**
|
|
/**
|
|
|
* 预取 Manager 数据:
|
|
* 预取 Manager 数据:
|
|
|
* - 内部员工: 钉钉 API 取 manager_userid (直属主管, 单值)
|
|
* - 内部员工: 钉钉 API 取 manager_userid (直属主管, 单值)
|
|
|
- * - 外部员工: 项目档案 assignments (按天算 PM 在 concurrentUpsert 内层, 多项目合并去重, 全空则不写入)
|
|
|
|
|
|
|
+ * - 全体员工: 匹配启用项目 assignments,作为应填报工时准入条件
|
|
|
|
|
+ * - 外部员工: 再按天从 assignments 计算 PM,多项目合并去重
|
|
|
* ppExt: 与前端 TimeCard 刻意不同 - 不做 PM 离职探活、不兜底 Raymond (后端只记数据, 非审批找活人)
|
|
* ppExt: 与前端 TimeCard 刻意不同 - 不做 PM 离职探活、不兜底 Raymond (后端只记数据, 非审批找活人)
|
|
|
*
|
|
*
|
|
|
* @param personnelMap 全量人员档案
|
|
* @param personnelMap 全量人员档案
|
|
|
- * @return ManagerData: internal=Map<userId, mgrUserId>, external=Map<userId, List<Assignment>>
|
|
|
|
|
|
|
+ * @return ManagerData: internal=内部主管, projects=全员启用项目参与关系, external=外部员工项目参与关系
|
|
|
*/
|
|
*/
|
|
|
@SuppressWarnings("unchecked")
|
|
@SuppressWarnings("unchecked")
|
|
|
private ManagerData queryManagerData(Map<String, Map<String, Object>> personnelMap) {
|
|
private ManagerData queryManagerData(Map<String, Map<String, Object>> personnelMap) {
|
|
@@ -1689,7 +1711,7 @@ public class WorkHoursCalcService {
|
|
|
String accessToken = ddClient.getAccessToken();
|
|
String accessToken = ddClient.getAccessToken();
|
|
|
int internalCount = 0;
|
|
int internalCount = 0;
|
|
|
|
|
|
|
|
- // 内部员工:钉钉直属主管;外部员工:先收集起来批量匹配项目档案 PM
|
|
|
|
|
|
|
+ // 内部员工取钉钉直属主管;同时记录外部员工集合,供项目匹配统计与 PM 计算
|
|
|
for (Map.Entry<String, Map<String, Object>> entry : personnelMap.entrySet()) {
|
|
for (Map.Entry<String, Map<String, Object>> entry : personnelMap.entrySet()) {
|
|
|
String empId = entry.getKey();
|
|
String empId = entry.getKey();
|
|
|
Object attr = entry.getValue().get("radioField_mkow4ydo");
|
|
Object attr = entry.getValue().get("radioField_mkow4ydo");
|
|
@@ -1712,27 +1734,31 @@ public class WorkHoursCalcService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 外部员工: 预取项目 assignments (供 concurrentUpsert 按天算 PM)
|
|
|
|
|
- if (!externalIds.isEmpty()) {
|
|
|
|
|
- md.external = queryProjectAssignments(externalIds);
|
|
|
|
|
|
|
+ // prd 全体员工都必须匹配启用项目;外部员工另保留子集用于兼容现有统计字段
|
|
|
|
|
+ md.projects = queryProjectAssignments(personnelMap.keySet());
|
|
|
|
|
+ for (String externalId : externalIds) {
|
|
|
|
|
+ List<Assignment> assignments = md.projects.get(externalId);
|
|
|
|
|
+ if (assignments != null && !assignments.isEmpty()) {
|
|
|
|
|
+ md.external.put(externalId, assignments);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- log.info("Manager 预取完成: 内部{}人取直属主管落地{}, 外部{}人预取项目档案({}人有项目)",
|
|
|
|
|
- internalCount, md.internal.size(), externalIds.size(), md.external.size());
|
|
|
|
|
|
|
+ log.info("Manager 预取完成: 内部{}人取直属主管落地{}, 全员启用项目参与者{}, 外部{}人中{}人有启用项目",
|
|
|
|
|
+ internalCount, md.internal.size(), md.projects.size(), externalIds.size(), md.external.size());
|
|
|
return md;
|
|
return md;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 扫项目档案全量, 收集外部员工的项目 assignments (含下线日期 + PM), 按天算 PM 见 computeDailyPms
|
|
|
|
|
|
|
+ * 查询状态为“启用”的项目,收集指定人员的 assignments(含下线日期 + PM)。
|
|
|
* ppExt: 子表 <50 行用内联、==50 走 ydService.queryDetails 取全 (与 ApprovalWriteBackService.resolveDetailRows 同款)
|
|
* ppExt: 子表 <50 行用内联、==50 走 ydService.queryDetails 取全 (与 ApprovalWriteBackService.resolveDetailRows 同款)
|
|
|
*
|
|
*
|
|
|
- * @param externalIds 外部员工 userId 集合
|
|
|
|
|
- * @return Map<外部员工 userId, List<Assignment>>, 仅含在项目子表出现过的外部员工
|
|
|
|
|
|
|
+ * @param personnelIds 待匹配人员 userId 集合
|
|
|
|
|
+ * @return Map<人员 userId, List<Assignment>>, 仅含启用项目子表中出现过的人员
|
|
|
*/
|
|
*/
|
|
|
@SuppressWarnings("unchecked")
|
|
@SuppressWarnings("unchecked")
|
|
|
- private Map<String, List<Assignment>> queryProjectAssignments(Set<String> externalIds) {
|
|
|
|
|
|
|
+ private Map<String, List<Assignment>> queryProjectAssignments(Set<String> personnelIds) {
|
|
|
Map<String, List<Assignment>> result = new HashMap<>();
|
|
Map<String, List<Assignment>> result = new HashMap<>();
|
|
|
- if (externalIds == null || externalIds.isEmpty()) {
|
|
|
|
|
|
|
+ if (personnelIds == null || personnelIds.isEmpty()) {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1748,9 +1774,11 @@ public class WorkHoursCalcService {
|
|
|
.appType(appType)
|
|
.appType(appType)
|
|
|
.systemToken(systemToken)
|
|
.systemToken(systemToken)
|
|
|
.formUuid(formUuidProject)
|
|
.formUuid(formUuidProject)
|
|
|
|
|
+ .searchFieldJson(JSON.toJSONString(
|
|
|
|
|
+ Collections.singletonMap(PROJECT_STATUS, PROJECT_STATUS_ACTIVE)))
|
|
|
.currentPage(currentPage)
|
|
.currentPage(currentPage)
|
|
|
.pageSize(pageSize)
|
|
.pageSize(pageSize)
|
|
|
- .build(), YDConf.FORM_QUERY.retrieve_list_all);
|
|
|
|
|
|
|
+ .build(), YDConf.FORM_QUERY.retrieve_search_form);
|
|
|
|
|
|
|
|
totalCount = page.getTotalCount();
|
|
totalCount = page.getTotalCount();
|
|
|
List<Map> dataList = (List<Map>) page.getData();
|
|
List<Map> dataList = (List<Map>) page.getData();
|
|
@@ -1758,16 +1786,18 @@ public class WorkHoursCalcService {
|
|
|
|
|
|
|
|
for (Map item : dataList) {
|
|
for (Map item : dataList) {
|
|
|
Object instIdObj = item.get("formInstanceId");
|
|
Object instIdObj = item.get("formInstanceId");
|
|
|
|
|
+ if (instIdObj == null) instIdObj = item.get("formInstId");
|
|
|
if (instIdObj == null) continue;
|
|
if (instIdObj == null) continue;
|
|
|
String projectInstId = String.valueOf(instIdObj);
|
|
String projectInstId = String.valueOf(instIdObj);
|
|
|
|
|
|
|
|
Map<String, Object> formData = (Map<String, Object>) item.get("formData");
|
|
Map<String, Object> formData = (Map<String, Object>) item.get("formData");
|
|
|
if (formData == null) continue;
|
|
if (formData == null) continue;
|
|
|
|
|
+ if (!PROJECT_STATUS_ACTIVE.equals(String.valueOf(formData.get(PROJECT_STATUS)).trim())) continue;
|
|
|
|
|
|
|
|
List<Map> subRows = resolveProjectMembers(projectInstId, (List<Map>) formData.get(PROJECT_SUB_TABLE));
|
|
List<Map> subRows = resolveProjectMembers(projectInstId, (List<Map>) formData.get(PROJECT_SUB_TABLE));
|
|
|
for (Map row : subRows) {
|
|
for (Map row : subRows) {
|
|
|
String memberId = extractEmployeeId(row, PROJECT_SUB_MEMBER);
|
|
String memberId = extractEmployeeId(row, PROJECT_SUB_MEMBER);
|
|
|
- if (memberId == null || !externalIds.contains(memberId)) continue;
|
|
|
|
|
|
|
+ if (memberId == null || !personnelIds.contains(memberId)) continue;
|
|
|
|
|
|
|
|
LocalDate offlineDate = parseToLocalDate(row.get(PROJECT_SUB_OFFLINE));
|
|
LocalDate offlineDate = parseToLocalDate(row.get(PROJECT_SUB_OFFLINE));
|
|
|
String pmId = extractEmployeeId(row, PROJECT_SUB_PM);
|
|
String pmId = extractEmployeeId(row, PROJECT_SUB_PM);
|
|
@@ -1781,6 +1811,21 @@ public class WorkHoursCalcService {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 当天至少存在一条尚未下线的启用项目参与关系。 */
|
|
|
|
|
+ private boolean hasActiveProject(List<Assignment> assignments, LocalDate workDay) {
|
|
|
|
|
+ if (assignments == null || assignments.isEmpty()) return false;
|
|
|
|
|
+ for (Assignment assignment : assignments) {
|
|
|
|
|
+ if (isAssignmentActive(assignment, workDay)) return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isAssignmentActive(Assignment assignment, LocalDate workDay) {
|
|
|
|
|
+ return assignment != null
|
|
|
|
|
+ && workDay != null
|
|
|
|
|
+ && (assignment.offlineDate == null || !workDay.isAfter(assignment.offlineDate));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 计算某员工在 workDay 当天参与的活跃项目 PM 集合 (去重, 排除下线日期已过 或 PM 为空的项目)
|
|
* 计算某员工在 workDay 当天参与的活跃项目 PM 集合 (去重, 排除下线日期已过 或 PM 为空的项目)
|
|
|
* ppExt: 只用下线日期判定 — 下线日期为 null 或 workDay <= offlineDate 视为当天在项目
|
|
* ppExt: 只用下线日期判定 — 下线日期为 null 或 workDay <= offlineDate 视为当天在项目
|
|
@@ -1791,15 +1836,16 @@ public class WorkHoursCalcService {
|
|
|
if (assignments == null || assignments.isEmpty()) return Collections.emptyList();
|
|
if (assignments == null || assignments.isEmpty()) return Collections.emptyList();
|
|
|
Set<String> pms = new LinkedHashSet<>();
|
|
Set<String> pms = new LinkedHashSet<>();
|
|
|
for (Assignment a : assignments) {
|
|
for (Assignment a : assignments) {
|
|
|
- if (a.offlineDate != null && workDay.isAfter(a.offlineDate)) continue;
|
|
|
|
|
|
|
+ if (!isAssignmentActive(a, workDay)) continue;
|
|
|
if (a.pmId != null && !a.pmId.isEmpty()) pms.add(a.pmId);
|
|
if (a.pmId != null && !a.pmId.isEmpty()) pms.add(a.pmId);
|
|
|
}
|
|
}
|
|
|
return new ArrayList<>(pms);
|
|
return new ArrayList<>(pms);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 内部员工 → 直属主管 userId; 外部员工 → 项目 assignments 明细. 供 concurrentUpsert 按天算 Manager */
|
|
|
|
|
|
|
+ /** 内部员工 → 直属主管;projects → 全员启用项目;external → 外部员工启用项目子集。 */
|
|
|
private static class ManagerData {
|
|
private static class ManagerData {
|
|
|
Map<String, String> internal = new HashMap<>();
|
|
Map<String, String> internal = new HashMap<>();
|
|
|
|
|
+ Map<String, List<Assignment>> projects = new HashMap<>();
|
|
|
Map<String, List<Assignment>> external = new HashMap<>();
|
|
Map<String, List<Assignment>> external = new HashMap<>();
|
|
|
}
|
|
}
|
|
|
|
|
|