feat(aihr): filter supervisor reviews by org scope
This commit is contained in:
+4
-2
@@ -53,8 +53,10 @@ public class AihrMobileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/practice/reviews")
|
@GetMapping("/practice/reviews")
|
||||||
public R<List<RecordResponse>> practiceReviews() {
|
public R<List<RecordResponse>> practiceReviews(@RequestParam(required = false) String projectCode,
|
||||||
return R.ok(mobileSeedService.practiceReviews(currentAppUsername()));
|
@RequestParam(required = false) String extPartyId,
|
||||||
|
@RequestParam(required = false) Integer limit) {
|
||||||
|
return R.ok(mobileSeedService.practiceReviews(currentAppUsername(), projectCode, extPartyId, limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/practice/reviews/{id}")
|
@GetMapping("/practice/reviews/{id}")
|
||||||
|
|||||||
+4
@@ -57,6 +57,10 @@ public class AihrMobileSeedService {
|
|||||||
return practiceSeedService.pendingReviewRecords(supervisorExtPartyId, 5);
|
return practiceSeedService.pendingReviewRecords(supervisorExtPartyId, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<RecordResponse> practiceReviews(String supervisorExtPartyId, String projectCode, String extPartyId, Integer limit) {
|
||||||
|
return practiceSeedService.pendingReviewRecords(supervisorExtPartyId, projectCode, extPartyId, limit == null ? 20 : limit);
|
||||||
|
}
|
||||||
|
|
||||||
public ReviewDetailResponse practiceReview(Long id) {
|
public ReviewDetailResponse practiceReview(Long id) {
|
||||||
return practiceSeedService.reviewDetail(id);
|
return practiceSeedService.reviewDetail(id);
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-16
@@ -478,37 +478,55 @@ public class AihrPracticeSeedService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<RecordResponse> pendingReviewRecords(int limit) {
|
public List<RecordResponse> pendingReviewRecords(int limit) {
|
||||||
ensurePracticeTable();
|
return pendingReviewRecords("", "", "", limit);
|
||||||
return jdbcTemplate.query("""
|
|
||||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id
|
|
||||||
FROM aihr_practice_session
|
|
||||||
WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'
|
|
||||||
ORDER BY CASE WHEN total_score < 80 THEN 0 ELSE 1 END, total_score ASC, finished_time DESC, id DESC
|
|
||||||
LIMIT ?
|
|
||||||
""", this::mapRecord, TENANT_ID, normalizeLimit(limit));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<RecordResponse> pendingReviewRecords(String supervisorExtPartyId, int limit) {
|
public List<RecordResponse> pendingReviewRecords(String supervisorExtPartyId, int limit) {
|
||||||
|
return pendingReviewRecords(supervisorExtPartyId, "", "", limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RecordResponse> pendingReviewRecords(String supervisorExtPartyId, String projectCode, String extPartyId, int limit) {
|
||||||
TeamScope scope = teamScope(supervisorExtPartyId);
|
TeamScope scope = teamScope(supervisorExtPartyId);
|
||||||
if (!scope.scoped()) {
|
if (scope.scoped() && scope.extPartyIds().isEmpty()) {
|
||||||
return pendingReviewRecords(limit);
|
return List.of();
|
||||||
}
|
}
|
||||||
if (scope.extPartyIds().isEmpty()) {
|
String project = isBlank(projectCode) ? "" : projectCode.trim();
|
||||||
|
if (!project.isEmpty() && !orgSnapshotTableExists()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
ensurePracticeTable();
|
ensurePracticeTable();
|
||||||
List<Object> args = new ArrayList<>();
|
List<Object> args = new ArrayList<>();
|
||||||
args.add(TENANT_ID);
|
args.add(TENANT_ID);
|
||||||
args.addAll(scope.extPartyIds());
|
StringBuilder where = new StringBuilder("WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'");
|
||||||
|
String party = isBlank(extPartyId) ? "" : extPartyId.trim();
|
||||||
|
if (!party.isEmpty()) {
|
||||||
|
where.append(" AND ext_party_id = ?");
|
||||||
|
args.add(party);
|
||||||
|
}
|
||||||
|
if (!project.isEmpty()) {
|
||||||
|
where.append("""
|
||||||
|
AND ext_party_id IN (
|
||||||
|
SELECT ext_party_id
|
||||||
|
FROM aihr_org_snapshot
|
||||||
|
WHERE tenant_id = ? AND project_code = ? AND employment_status = 'active'
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
args.add(TENANT_ID);
|
||||||
|
args.add(project);
|
||||||
|
}
|
||||||
|
if (scope.scoped()) {
|
||||||
|
where.append(" AND ext_party_id IN (").append(inClause(scope.extPartyIds().size())).append(")");
|
||||||
|
args.addAll(scope.extPartyIds());
|
||||||
|
}
|
||||||
args.add(normalizeLimit(limit));
|
args.add(normalizeLimit(limit));
|
||||||
return jdbcTemplate.query("""
|
String sql = """
|
||||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id
|
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id
|
||||||
FROM aihr_practice_session
|
FROM aihr_practice_session
|
||||||
WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'
|
""" + where + """
|
||||||
AND ext_party_id IN (%s)
|
|
||||||
ORDER BY CASE WHEN total_score < 80 THEN 0 ELSE 1 END, total_score ASC, finished_time DESC, id DESC
|
ORDER BY CASE WHEN total_score < 80 THEN 0 ELSE 1 END, total_score ASC, finished_time DESC, id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""".formatted(inClause(scope.extPartyIds().size())), this::mapRecord, args.toArray());
|
""";
|
||||||
|
return jdbcTemplate.query(sql, this::mapRecord, args.toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String exportPilotCsv() {
|
public String exportPilotCsv() {
|
||||||
|
|||||||
+97
-5
@@ -3,6 +3,7 @@ package org.dromara.aihr.service;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentRequest;
|
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentRequest;
|
||||||
import org.dromara.common.core.exception.ServiceException;
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.ArgumentMatchers;
|
import org.mockito.ArgumentMatchers;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
@@ -11,17 +12,20 @@ import org.springframework.jdbc.core.RowMapper;
|
|||||||
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.contains;
|
import static org.mockito.ArgumentMatchers.contains;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@Tag("dev")
|
||||||
public class AihrPracticeSeedServiceTest {
|
public class AihrPracticeSeedServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -64,13 +68,101 @@ public class AihrPracticeSeedServiceTest {
|
|||||||
assertEquals("只能派发给当前项目范围内员工", error.getMessage());
|
assertEquals("只能派发给当前项目范围内员工", error.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pendingReviewRecordsAppliesProjectAndAssigneeFilters() {
|
||||||
|
PracticeReviewJdbcTemplate jdbcTemplate = new PracticeReviewJdbcTemplate("主管", List.of("SUP-1", "EMP-1"));
|
||||||
|
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
|
||||||
|
|
||||||
|
service.pendingReviewRecords("SUP-1", "P1", "EMP-1", 50);
|
||||||
|
|
||||||
|
assertTrue(jdbcTemplate.reviewSql.contains("status = '待复盘'"));
|
||||||
|
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id = ?"));
|
||||||
|
assertTrue(jdbcTemplate.reviewSql.contains("project_code = ?"));
|
||||||
|
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id IN (?,?)"));
|
||||||
|
assertEquals(List.of("000000", "EMP-1", "000000", "P1", "SUP-1", "EMP-1", 20), jdbcTemplate.reviewArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pendingReviewRecordsLimitsNonSupervisorToSelf() {
|
||||||
|
PracticeReviewJdbcTemplate jdbcTemplate = new PracticeReviewJdbcTemplate("员工", List.of("SUP-1", "EMP-1"));
|
||||||
|
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
|
||||||
|
|
||||||
|
service.pendingReviewRecords("SUP-1", "", "", 0);
|
||||||
|
|
||||||
|
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id IN (?)"));
|
||||||
|
assertEquals(List.of("000000", "SUP-1", 5), jdbcTemplate.reviewArgs);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
private static <T> List<T> mapOne(InvocationOnMock invocation, Map<String, String> values) throws SQLException {
|
private static <T> List<T> mapOne(InvocationOnMock invocation, Map<String, String> values) throws SQLException {
|
||||||
RowMapper mapper = invocation.getArgument(1);
|
RowMapper mapper = invocation.getArgument(1);
|
||||||
ResultSet resultSet = mock(ResultSet.class);
|
return mapRows(mapper, List.of(values));
|
||||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
|
||||||
when(resultSet.getString(entry.getKey())).thenReturn(entry.getValue());
|
|
||||||
}
|
|
||||||
return List.of((T) mapper.mapRow(resultSet, 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
private static <T> List<T> mapRows(RowMapper mapper, List<Map<String, String>> rows) throws SQLException {
|
||||||
|
List<T> mapped = new ArrayList<>();
|
||||||
|
int index = 0;
|
||||||
|
for (Map<String, String> values : rows) {
|
||||||
|
ResultSet resultSet = mock(ResultSet.class);
|
||||||
|
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||||
|
when(resultSet.getString(entry.getKey())).thenReturn(entry.getValue());
|
||||||
|
}
|
||||||
|
mapped.add((T) mapper.mapRow(resultSet, index++));
|
||||||
|
}
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PracticeReviewJdbcTemplate extends JdbcTemplate {
|
||||||
|
private final String ownerLevel;
|
||||||
|
private final List<String> scopeIds;
|
||||||
|
private String reviewSql = "";
|
||||||
|
private List<Object> reviewArgs = List.of();
|
||||||
|
|
||||||
|
private PracticeReviewJdbcTemplate(String ownerLevel, List<String> scopeIds) {
|
||||||
|
this.ownerLevel = ownerLevel;
|
||||||
|
this.scopeIds = scopeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T queryForObject(String sql, Class<T> requiredType) {
|
||||||
|
return requiredType.cast(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T queryForObject(String sql, Class<T> requiredType, Object... args) {
|
||||||
|
return requiredType.cast(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(String sql) {
|
||||||
|
// Table creation is irrelevant for query assembly tests.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
|
||||||
|
try {
|
||||||
|
if (sql.contains("FROM aihr_practice_session")) {
|
||||||
|
reviewSql = sql;
|
||||||
|
reviewArgs = List.of(args);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (sql.contains("SELECT project_code") && sql.contains("LIMIT 1")) {
|
||||||
|
return mapRows(rowMapper, List.of(Map.of(
|
||||||
|
"project_code", "P1",
|
||||||
|
"position_level", ownerLevel
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if (sql.contains("SELECT ext_party_id") && sql.contains("ORDER BY position_level")) {
|
||||||
|
return mapRows(rowMapper, scopeIds.stream()
|
||||||
|
.map(id -> Map.of("ext_party_id", id))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
| 优先级 | 差异项 | 当前状态 | 本次实现 | 验收口径 |
|
| 优先级 | 差异项 | 当前状态 | 本次实现 | 验收口径 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| P0 | 组织人员 / 岗位 / 项目权限运营页 | 组织同步 API 存在,侧栏未暴露运营视图 | 新增 `组织与权限` 页面,呈现组织人员快照、岗位映射、项目数据范围和同步预检入口 | PC 可查询人员快照,看到身份三轴、权限映射、同步结果边界 |
|
| P0 | 组织人员 / 岗位 / 项目权限运营页 | 组织同步 API 存在,侧栏未暴露运营视图 | 新增 `组织与权限` 页面,呈现组织人员快照、岗位映射、项目数据范围和同步预检入口 | PC 可查询人员快照,看到身份三轴、权限映射、同步结果边界 |
|
||||||
| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 后端已有复盘、评分校准、派发接口;PC 只有散落入口 | 新增 `复盘校准` 页面,集中待复盘列表、详情、复盘建议、学分、下次训练派发 | 主管能从一页完成“看记录 -> 写建议 -> 派训练” |
|
| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 后端已有复盘、评分校准、派发接口;PC 只有散落入口 | 新增 `复盘校准` 页面,并由后端基于组织快照做项目/人员筛选 | 主管能从一页完成“看记录 -> 写建议 -> 派训练” |
|
||||||
| P0 | 能力画像 / 等级证据包 / 激励规则 | 后端已有画像和证据包接口;PC 无独立页 | 新增 `成长激励` 页面,展示画像、等级路径、证据包、预警、任务与最小规则表 | HR 能看到初/中/高级证据和学分规则口径 |
|
| P0 | 能力画像 / 等级证据包 / 激励规则 | 后端已有画像和证据包接口;PC 无独立页 | 新增 `成长激励` 页面,并用组织快照选择员工查看画像和派发专项 | HR 能看到初/中/高级证据和学分规则口径 |
|
||||||
| P1 | 学模块课程 / 考试后台 | 文档要求阶段二建设;当前无页面 | 暂不实现,保留 TODO | 阶段二专项设计后再建课程、课件、考试、成绩表 |
|
| P1 | 学模块课程 / 考试后台 | 文档要求阶段二建设;当前无页面 | 暂不实现,保留 TODO | 阶段二专项设计后再建课程、课件、考试、成绩表 |
|
||||||
| P1 | 大喇叭文件分发 | 总纲阶段二;当前无页面 | 暂不实现,保留 TODO | 半年会后按身份三轴做摘要、已读未读、补推 |
|
| P1 | 大喇叭文件分发 | 总纲阶段二;当前无页面 | 暂不实现,保留 TODO | 半年会后按身份三轴做摘要、已读未读、补推 |
|
||||||
| P1 | 开放问题榜 | 阶段一只埋 `knowledge_gap`;当前无 UI | 暂不实现,保留 TODO | 阶段二消费 gap 池、上榜、最佳答案、沉淀入库 |
|
| P1 | 开放问题榜 | 阶段一只埋 `knowledge_gap`;当前无 UI | 暂不实现,保留 TODO | 阶段二消费 gap 池、上榜、最佳答案、沉淀入库 |
|
||||||
@@ -19,4 +19,5 @@
|
|||||||
- 不新建数据库表。
|
- 不新建数据库表。
|
||||||
- 不改登录、密钥、模型配置、支付、部署脚本。
|
- 不改登录、密钥、模型配置、支付、部署脚本。
|
||||||
- 优先复用已有 `/api/aihr/mobile/practice/*` 与 `/api/train/practice/*` 接口。
|
- 优先复用已有 `/api/aihr/mobile/practice/*` 与 `/api/train/practice/*` 接口。
|
||||||
|
- 组织人员快照在 `组织与权限` 页分页展示;业务页只取下拉候选,真实复盘范围由接口参数落到后端过滤。
|
||||||
- 规则配置中心先做运营口径展示;需要真实持久化时再补 `incentive_rule` 等表。
|
- 规则配置中心先做运营口径展示;需要真实持久化时再补 `incentive_rule` 等表。
|
||||||
|
|||||||
@@ -154,10 +154,17 @@ export type OrgSnapshotResponse = {
|
|||||||
rows: OrgPersonRow[];
|
rows: OrgPersonRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function listPracticeReviews(): Promise<ApiResult<ReviewRecord[]>> {
|
export type ReviewListParams = {
|
||||||
|
projectCode?: string;
|
||||||
|
extPartyId?: string;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function listPracticeReviews(params?: ReviewListParams): Promise<ApiResult<ReviewRecord[]>> {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/aihr/mobile/practice/reviews',
|
url: '/api/aihr/mobile/practice/reviews',
|
||||||
method: 'get'
|
method: 'get',
|
||||||
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,17 @@
|
|||||||
<p>展示能力画像、初/中/高级证据包、预警、待训任务和学分规则最小集。</p>
|
<p>展示能力画像、初/中/高级证据包、预警、待训任务和学分规则最小集。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="head-actions">
|
<div class="head-actions">
|
||||||
<el-input v-model="extPartyId" clearable placeholder="员工手机号或 extPartyId" />
|
<el-select v-model="selectedProjectCode" clearable filterable placeholder="项目范围">
|
||||||
|
<el-option v-for="item in orgSnapshot?.projects || []" :key="item.projectCode" :label="item.projectName" :value="item.projectCode" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="extPartyId" clearable filterable allow-create default-first-option placeholder="组织人员或 extPartyId">
|
||||||
|
<el-option
|
||||||
|
v-for="person in orgPeople"
|
||||||
|
:key="person.extPartyId"
|
||||||
|
:label="`${person.personName} / ${person.positionName}`"
|
||||||
|
:value="person.extPartyId"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
<el-button type="danger" :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
|
<el-button type="danger" :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -123,27 +133,31 @@
|
|||||||
<script setup name="CompetencyGrowth" lang="ts">
|
<script setup name="CompetencyGrowth" lang="ts">
|
||||||
import { Refresh } from '@element-plus/icons-vue';
|
import { Refresh } from '@element-plus/icons-vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { listPracticeScenarios } from '@/api/aihr/practice';
|
import { listPracticeScenarios } from '@/api/aihr/practice';
|
||||||
import {
|
import {
|
||||||
createPracticeAssignment,
|
createPracticeAssignment,
|
||||||
getCompetencyProfile,
|
getCompetencyProfile,
|
||||||
|
getOrgSnapshot,
|
||||||
getPromotionEvidence,
|
getPromotionEvidence,
|
||||||
listPracticeAlerts,
|
listPracticeAlerts,
|
||||||
listPracticeAssignments,
|
listPracticeAssignments,
|
||||||
type CompetencyProfile,
|
type CompetencyProfile,
|
||||||
|
type OrgSnapshotResponse,
|
||||||
type PracticeAlert,
|
type PracticeAlert,
|
||||||
type PracticeAssignment,
|
type PracticeAssignment,
|
||||||
type PromotionEvidence
|
type PromotionEvidence
|
||||||
} from '@/api/aihr/mobile-admin';
|
} from '@/api/aihr/mobile-admin';
|
||||||
|
|
||||||
const extPartyId = ref('');
|
const extPartyId = ref('');
|
||||||
|
const selectedProjectCode = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const leaderboardEnabled = ref(false);
|
const leaderboardEnabled = ref(false);
|
||||||
const profile = ref<CompetencyProfile | null>(null);
|
const profile = ref<CompetencyProfile | null>(null);
|
||||||
const evidence = ref<PromotionEvidence | null>(null);
|
const evidence = ref<PromotionEvidence | null>(null);
|
||||||
const alerts = ref<PracticeAlert[]>([]);
|
const alerts = ref<PracticeAlert[]>([]);
|
||||||
const assignments = ref<PracticeAssignment[]>([]);
|
const assignments = ref<PracticeAssignment[]>([]);
|
||||||
|
const orgSnapshot = ref<OrgSnapshotResponse | null>(null);
|
||||||
|
|
||||||
const rules = [
|
const rules = [
|
||||||
{ source: '训练完成', points: '+5', guard: '只认完成记录,不按点击或打开计分' },
|
{ source: '训练完成', points: '+5', guard: '只认完成记录,不按点击或打开计分' },
|
||||||
@@ -152,6 +166,23 @@ const rules = [
|
|||||||
{ source: '课程考试通过', points: '+10', guard: '阶段二课程考试上线后启用' }
|
{ source: '课程考试通过', points: '+10', guard: '阶段二课程考试上线后启用' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const orgPeople = computed(() => orgSnapshot.value?.rows || []);
|
||||||
|
const selectedPerson = computed(() => orgPeople.value.find((item) => item.extPartyId === extPartyId.value));
|
||||||
|
|
||||||
|
const loadOrgSnapshot = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getOrgSnapshot({
|
||||||
|
status: 'active',
|
||||||
|
projectCode: selectedProjectCode.value,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 500
|
||||||
|
});
|
||||||
|
orgSnapshot.value = result.data;
|
||||||
|
} catch (error) {
|
||||||
|
orgSnapshot.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadAll = async () => {
|
const loadAll = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const party = extPartyId.value.trim();
|
const party = extPartyId.value.trim();
|
||||||
@@ -184,7 +215,7 @@ const createManualAssignment = async () => {
|
|||||||
const scenario = scenarios.find((item) => item.enabled) || scenarios[0];
|
const scenario = scenarios.find((item) => item.enabled) || scenarios[0];
|
||||||
await createPracticeAssignment({
|
await createPracticeAssignment({
|
||||||
extPartyId: party,
|
extPartyId: party,
|
||||||
traineeName: party,
|
traineeName: selectedPerson.value?.personName || party,
|
||||||
scenarioId: scenario?.id || 'fee-parking',
|
scenarioId: scenario?.id || 'fee-parking',
|
||||||
scenarioName: scenario?.name,
|
scenarioName: scenario?.name,
|
||||||
source: 'manual',
|
source: 'manual',
|
||||||
@@ -197,7 +228,19 @@ const createManualAssignment = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(loadAll);
|
watch(selectedProjectCode, async () => {
|
||||||
|
extPartyId.value = '';
|
||||||
|
await loadOrgSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(extPartyId, () => {
|
||||||
|
loadAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOrgSnapshot();
|
||||||
|
loadAll();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -236,7 +279,7 @@ onMounted(loadAll);
|
|||||||
|
|
||||||
.head-actions {
|
.head-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 260px auto;
|
grid-template-columns: 220px 280px auto;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,6 +400,8 @@ onMounted(loadAll);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.head-actions {
|
.head-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,22 @@
|
|||||||
<article class="panel list-panel">
|
<article class="panel list-panel">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<h2>待复盘记录</h2>
|
<h2>待复盘记录</h2>
|
||||||
<el-tag type="danger">{{ reviews.length }} 条</el-tag>
|
<el-tag type="danger">{{ visibleReviews.length }} 条</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-loading="loading" :data="reviews" size="small" highlight-current-row @row-click="selectReview">
|
<div class="list-filters">
|
||||||
|
<el-select v-model="selectedProjectCode" clearable filterable placeholder="项目范围">
|
||||||
|
<el-option v-for="item in orgSnapshot?.projects || []" :key="item.projectCode" :label="item.projectName" :value="item.projectCode" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="selectedExtPartyId" clearable filterable placeholder="组织人员">
|
||||||
|
<el-option
|
||||||
|
v-for="person in orgPeople"
|
||||||
|
:key="person.extPartyId"
|
||||||
|
:label="`${person.personName} / ${person.positionName}`"
|
||||||
|
:value="person.extPartyId"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="loading" :data="visibleReviews" size="small" highlight-current-row @row-click="selectReview">
|
||||||
<el-table-column prop="trainee" label="学员" width="96" />
|
<el-table-column prop="trainee" label="学员" width="96" />
|
||||||
<el-table-column prop="scene" label="场景" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="scene" label="场景" min-width="150" show-overflow-tooltip />
|
||||||
<el-table-column prop="score" label="得分" width="70" />
|
<el-table-column prop="score" label="得分" width="70" />
|
||||||
@@ -92,15 +105,26 @@
|
|||||||
<script setup name="TrainReviews" lang="ts">
|
<script setup name="TrainReviews" lang="ts">
|
||||||
import { Refresh } from '@element-plus/icons-vue';
|
import { Refresh } from '@element-plus/icons-vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
|
import { listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
|
||||||
import { getPracticeReview, listPracticeReviews, markPracticeReviewed, type ReviewDetail, type ReviewRecord } from '@/api/aihr/mobile-admin';
|
import {
|
||||||
|
getOrgSnapshot,
|
||||||
|
getPracticeReview,
|
||||||
|
listPracticeReviews,
|
||||||
|
markPracticeReviewed,
|
||||||
|
type OrgSnapshotResponse,
|
||||||
|
type ReviewDetail,
|
||||||
|
type ReviewRecord
|
||||||
|
} from '@/api/aihr/mobile-admin';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const reviews = ref<ReviewRecord[]>([]);
|
const reviews = ref<ReviewRecord[]>([]);
|
||||||
const detail = ref<ReviewDetail | null>(null);
|
const detail = ref<ReviewDetail | null>(null);
|
||||||
const scenarios = ref<PracticeScenarioResponse[]>([]);
|
const scenarios = ref<PracticeScenarioResponse[]>([]);
|
||||||
|
const orgSnapshot = ref<OrgSnapshotResponse | null>(null);
|
||||||
|
const selectedProjectCode = ref('');
|
||||||
|
const selectedExtPartyId = ref('');
|
||||||
|
|
||||||
const reviewForm = reactive({
|
const reviewForm = reactive({
|
||||||
reviewer: '主管',
|
reviewer: '主管',
|
||||||
@@ -110,6 +134,9 @@ const reviewForm = reactive({
|
|||||||
nextReason: ''
|
nextReason: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const orgPeople = computed(() => orgSnapshot.value?.rows || []);
|
||||||
|
const visibleReviews = computed(() => reviews.value);
|
||||||
|
|
||||||
const loadScenarios = async () => {
|
const loadScenarios = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await listPracticeScenarios();
|
const result = await listPracticeScenarios();
|
||||||
@@ -119,14 +146,30 @@ const loadScenarios = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadOrgSnapshot = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getOrgSnapshot({
|
||||||
|
status: 'active',
|
||||||
|
projectCode: selectedProjectCode.value,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 500
|
||||||
|
});
|
||||||
|
orgSnapshot.value = result.data;
|
||||||
|
} catch (error) {
|
||||||
|
orgSnapshot.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadReviews = async () => {
|
const loadReviews = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await listPracticeReviews();
|
const result = await listPracticeReviews({
|
||||||
|
projectCode: selectedProjectCode.value,
|
||||||
|
extPartyId: selectedExtPartyId.value,
|
||||||
|
limit: 50
|
||||||
|
});
|
||||||
reviews.value = result.data || [];
|
reviews.value = result.data || [];
|
||||||
if (!detail.value && reviews.value.length > 0) {
|
await selectFirstVisibleReview();
|
||||||
await selectReview(reviews.value[0]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('待复盘列表加载失败');
|
ElMessage.error('待复盘列表加载失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -143,6 +186,16 @@ const selectReview = async (row: ReviewRecord) => {
|
|||||||
reviewForm.nextReason = '';
|
reviewForm.nextReason = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectFirstVisibleReview = async () => {
|
||||||
|
if (visibleReviews.value.some((item) => item.id === detail.value?.id)) return;
|
||||||
|
const first = visibleReviews.value[0];
|
||||||
|
if (!first) {
|
||||||
|
detail.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await selectReview(first);
|
||||||
|
};
|
||||||
|
|
||||||
const submitReview = async () => {
|
const submitReview = async () => {
|
||||||
if (!detail.value) return;
|
if (!detail.value) return;
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
@@ -164,8 +217,19 @@ const submitReview = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watch(selectedProjectCode, async () => {
|
||||||
|
selectedExtPartyId.value = '';
|
||||||
|
await loadOrgSnapshot();
|
||||||
|
await loadReviews();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(selectedExtPartyId, () => {
|
||||||
|
loadReviews();
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadScenarios();
|
loadScenarios();
|
||||||
|
loadOrgSnapshot();
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -227,6 +291,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-filters {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.score-grid,
|
.score-grid,
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -287,6 +358,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
@media (max-width: 1080px) {
|
@media (max-width: 1080px) {
|
||||||
.review-layout,
|
.review-layout,
|
||||||
|
.list-filters,
|
||||||
.score-grid,
|
.score-grid,
|
||||||
.form-grid {
|
.form-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user