feat(aihr): link candidates to employee principals
This commit is contained in:
@@ -72,6 +72,13 @@ export type InterviewRecord = {
|
||||
summary: string;
|
||||
};
|
||||
|
||||
export type CandidateLinkResponse = {
|
||||
candidateId: string;
|
||||
extPartyId: string;
|
||||
status: string;
|
||||
linkedTime: string;
|
||||
};
|
||||
|
||||
export type CandidateMaterialReview = {
|
||||
id: number | string;
|
||||
candidateId: string;
|
||||
@@ -119,6 +126,23 @@ export function listInterviewRecords(limit = 20): Promise<ApiResult<InterviewRec
|
||||
});
|
||||
}
|
||||
|
||||
export function getCandidateEmployeeLink(candidateId: string): Promise<ApiResult<CandidateLinkResponse | null>> {
|
||||
return request({
|
||||
url: '/api/recruit/interview/candidate-links',
|
||||
method: 'get',
|
||||
params: { candidateId }
|
||||
});
|
||||
}
|
||||
|
||||
export function linkCandidateEmployee(candidateId: string, extPartyId: string): Promise<ApiResult<CandidateLinkResponse>> {
|
||||
return request({
|
||||
url: '/api/recruit/interview/candidate-links',
|
||||
method: 'post',
|
||||
data: { candidateId, extPartyId },
|
||||
headers: { repeatSubmit: false }
|
||||
});
|
||||
}
|
||||
|
||||
export function listCandidateMaterialsForReview(status = ''): Promise<ApiResult<CandidateMaterialReview[]>> {
|
||||
return request({
|
||||
url: '/api/aihr/hr/candidate/materials',
|
||||
|
||||
@@ -47,6 +47,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="candidate-link-card">
|
||||
<div class="link-card-head">
|
||||
<div>
|
||||
<span>入职主体关联</span>
|
||||
<small>只绑定外部员工主体 ID,不复制组织姓名或部门</small>
|
||||
</div>
|
||||
<el-tag v-if="candidateLink" type="success" size="small">{{ candidateLink.status }}</el-tag>
|
||||
</div>
|
||||
<div v-if="candidateLink" class="linked-party">{{ candidateLink.extPartyId }} · {{ candidateLink.linkedTime }}</div>
|
||||
<div class="link-card-actions">
|
||||
<el-input v-model="linkExtPartyId" size="small" placeholder="输入已同步的 ext_party_id" clearable />
|
||||
<el-button type="danger" size="small" :loading="linkLoading" @click="submitCandidateLink">关联</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="job-card">
|
||||
<span>岗位画像</span>
|
||||
<p>{{ selectedCandidate.job.description }}</p>
|
||||
@@ -207,18 +222,21 @@
|
||||
</template>
|
||||
|
||||
<script setup name="RecruitInterview" lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { DocumentChecked, Reading, Refresh, Tickets, TrendCharts, UserFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
finishInterview,
|
||||
getCandidateEmployeeLink,
|
||||
linkCandidateEmployee,
|
||||
listCandidateMaterialsForReview,
|
||||
listInterviewRecords,
|
||||
reviewCandidateMaterial,
|
||||
startInterview,
|
||||
submitInterviewAnswer,
|
||||
type CandidateMaterialReview,
|
||||
type CandidateLinkResponse,
|
||||
type InterviewFinishResponse,
|
||||
type InterviewRecord
|
||||
} from '@/api/aihr/interview';
|
||||
@@ -412,6 +430,9 @@ const materialReviews = ref<CandidateMaterialReview[]>([]);
|
||||
const materialStatus = ref('待审核');
|
||||
const materialLoading = ref(false);
|
||||
const materialStatusOptions = ['全部', '待审核', '已通过', '已驳回'];
|
||||
const candidateLink = ref<CandidateLinkResponse | null>(null);
|
||||
const linkExtPartyId = ref('');
|
||||
const linkLoading = ref(false);
|
||||
|
||||
const selectedCandidate = computed(() => candidates.find((candidate) => candidate.id === candidateId.value) ?? candidates[0]);
|
||||
const questions = computed(() => (apiQuestions.value.length > 0 ? apiQuestions.value : selectedCandidate.value.questions));
|
||||
@@ -605,6 +626,37 @@ const loadInterviewRecords = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadCandidateLink = async () => {
|
||||
candidateLink.value = null;
|
||||
linkExtPartyId.value = '';
|
||||
try {
|
||||
const { data } = await getCandidateEmployeeLink(candidateId.value);
|
||||
candidateLink.value = data;
|
||||
linkExtPartyId.value = data?.extPartyId ?? '';
|
||||
} catch {
|
||||
// Candidate linkage is optional until the HR process reaches onboarding.
|
||||
}
|
||||
};
|
||||
|
||||
const submitCandidateLink = async () => {
|
||||
const extPartyId = linkExtPartyId.value.trim();
|
||||
if (!extPartyId) {
|
||||
ElMessage.warning('请输入已同步的员工主体 ID');
|
||||
return;
|
||||
}
|
||||
linkLoading.value = true;
|
||||
try {
|
||||
const { data } = await linkCandidateEmployee(candidateId.value, extPartyId);
|
||||
candidateLink.value = data;
|
||||
linkExtPartyId.value = data.extPartyId;
|
||||
ElMessage.success('候选人已关联员工主体');
|
||||
} catch {
|
||||
ElMessage.error('关联失败,请确认主体已在职组织快照中');
|
||||
} finally {
|
||||
linkLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMaterialReviews = async () => {
|
||||
materialLoading.value = true;
|
||||
try {
|
||||
@@ -639,7 +691,10 @@ const materialTagType = (status: string) => {
|
||||
onMounted(() => {
|
||||
loadInterviewRecords();
|
||||
loadMaterialReviews();
|
||||
loadCandidateLink();
|
||||
});
|
||||
|
||||
watch(candidateId, loadCandidateLink);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -844,6 +899,53 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.candidate-link-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e5eaf3;
|
||||
border-radius: 8px;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.link-card-head,
|
||||
.link-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.link-card-head {
|
||||
span,
|
||||
small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #172033;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #68758a;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.linked-party {
|
||||
color: #3c577c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.link-card-actions {
|
||||
:deep(.el-input) {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user