志愿
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
export const admissionPhases = new Set(['draft', 'filling', 'closed', 'matching', 'school_review', 'supplementary', 'completed']);
|
||||
|
||||
export function admissionRecords(db, kind, examId = null) {
|
||||
return (db.admissionRecords || []).filter(item => item.kind === kind && (!examId || item.examId === examId));
|
||||
}
|
||||
|
||||
export function admissionSetting(db, examId) {
|
||||
return admissionRecords(db, 'setting', examId)[0] || null;
|
||||
}
|
||||
|
||||
export function candidateTotalScore(db, examId, userId) {
|
||||
const registration = db.registrations.find(item => item.examId === examId && item.userId === userId && item.status === 'approved');
|
||||
if (!registration) return null;
|
||||
const results = db.results.filter(item => item.registrationId === registration.id && item.published);
|
||||
if (!registration.subjectIds.length || registration.subjectIds.some(id => !results.some(result => result.subjectId === id))) return null;
|
||||
return Number(results.reduce((sum, item) => sum + Number(item.score || 0), 0).toFixed(2));
|
||||
}
|
||||
|
||||
export function activePreference(db, examId, userId, round) {
|
||||
return admissionRecords(db, 'preference', examId).find(item => item.userId === userId && Number(item.payload?.round || 1) === Number(round || 1)) || null;
|
||||
}
|
||||
|
||||
export function approvedPlans(db, examId) {
|
||||
return admissionRecords(db, 'plan', examId).filter(item => item.status === 'approved');
|
||||
}
|
||||
|
||||
export function planSummary(plan) {
|
||||
const categories = Array.isArray(plan.payload?.categories) ? plan.payload.categories : [];
|
||||
return { ...plan, totalQuota: categories.reduce((sum, item) => sum + Number(item.quota || 0), 0) };
|
||||
}
|
||||
|
||||
export function publicAdmissionRows(db, examId) {
|
||||
return admissionRecords(db, 'placement', examId).filter(item => item.status === 'final').map(item => {
|
||||
const user = db.users.find(entry => entry.id === item.userId) || {};
|
||||
const profile = db.candidateProfiles.find(entry => entry.userId === item.userId) || {};
|
||||
const school = db.schools.find(entry => entry.id === item.schoolId) || {};
|
||||
return {
|
||||
registrationNumber: user.candidateNumber || '',
|
||||
name: profile.name || user.displayName || '',
|
||||
totalScore: Number(item.payload?.totalScore || 0),
|
||||
admittedSchool: school.name || '',
|
||||
categoryName: item.payload?.categoryName || '',
|
||||
idNumberMasked: profile.idNumber ? `${profile.idNumber.slice(0, 3)}***********${profile.idNumber.slice(-2)}` : '',
|
||||
phoneMasked: profile.phone ? `${profile.phone.slice(0, 3)}****${profile.phone.slice(-4)}` : ''
|
||||
};
|
||||
}).sort((a, b) => b.totalScore - a.totalScore || a.registrationNumber.localeCompare(b.registrationNumber));
|
||||
}
|
||||
|
||||
function categoryKey(schoolId, code) {
|
||||
return `${schoolId}|${code}`;
|
||||
}
|
||||
|
||||
export function buildVolunteerPlacements(db, setting, { uid, nowIso }) {
|
||||
const examId = setting.examId;
|
||||
const round = Number(setting.payload?.round || 1);
|
||||
const plans = approvedPlans(db, examId);
|
||||
const categories = new Map();
|
||||
for (const plan of plans) for (const category of plan.payload?.categories || []) {
|
||||
categories.set(categoryKey(plan.schoolId, category.code), { plan, category });
|
||||
}
|
||||
|
||||
const existing = admissionRecords(db, 'placement', examId).filter(item => item.status !== 'withdrawn');
|
||||
const occupied = new Map();
|
||||
const occupiedIndicators = new Map();
|
||||
const occupiedGeneral = new Map();
|
||||
for (const placement of existing) {
|
||||
const key = categoryKey(placement.schoolId, placement.payload?.categoryCode);
|
||||
occupied.set(key, (occupied.get(key) || 0) + 1);
|
||||
if (placement.payload?.quotaBucket?.startsWith('indicator:')) {
|
||||
const indicatorKey = `${key}|${placement.payload.quotaBucket.slice(10)}`;
|
||||
occupiedIndicators.set(indicatorKey, (occupiedIndicators.get(indicatorKey) || 0) + 1);
|
||||
} else occupiedGeneral.set(key, (occupiedGeneral.get(key) || 0) + 1);
|
||||
}
|
||||
|
||||
const preferences = admissionRecords(db, 'preference', examId).filter(item => Number(item.payload?.round || 1) === round && item.status === 'submitted');
|
||||
const candidates = preferences.map(preference => {
|
||||
const profile = db.candidateProfiles.find(item => item.userId === preference.userId) || {};
|
||||
const account = db.users.find(item => item.id === preference.userId) || {};
|
||||
return { preference, profile, account, score: candidateTotalScore(db, examId, preference.userId) };
|
||||
}).filter(item => item.score != null && !existing.some(entry => entry.userId === item.preference.userId && ['school_review', 'admitted', 'final', 'withdrawal_pending'].includes(entry.status)))
|
||||
.sort((left, right) => right.score - left.score || String(left.account.candidateNumber || '').localeCompare(String(right.account.candidateNumber || '')));
|
||||
|
||||
const created = [];
|
||||
for (const candidate of candidates) {
|
||||
const specialtyTypes = Array.isArray(candidate.profile.specialtyTypes) ? candidate.profile.specialtyTypes : [];
|
||||
for (const [index, choice] of (candidate.preference.payload?.choices || []).entries()) {
|
||||
const target = categories.get(categoryKey(choice.schoolId, choice.categoryCode));
|
||||
if (!target) continue;
|
||||
const { category } = target;
|
||||
if (category.specialtyType && !specialtyTypes.includes(category.specialtyType)) continue;
|
||||
const key = categoryKey(choice.schoolId, choice.categoryCode);
|
||||
if ((occupied.get(key) || 0) >= Number(category.quota || 0)) continue;
|
||||
const allocation = (category.indicatorAllocations || []).find(item => item.sourceSchoolId === candidate.profile.schoolId);
|
||||
let quotaBucket = null;
|
||||
if (allocation) {
|
||||
const indicatorKey = `${key}|${candidate.profile.schoolId}`;
|
||||
if ((occupiedIndicators.get(indicatorKey) || 0) < Number(allocation.quota || 0)) {
|
||||
quotaBucket = `indicator:${candidate.profile.schoolId}`;
|
||||
occupiedIndicators.set(indicatorKey, (occupiedIndicators.get(indicatorKey) || 0) + 1);
|
||||
}
|
||||
}
|
||||
if (!quotaBucket) {
|
||||
const generalQuota = Math.max(0, Number(category.quota || 0) - (category.indicatorAllocations || []).reduce((sum, item) => sum + Number(item.quota || 0), 0));
|
||||
if ((occupiedGeneral.get(key) || 0) >= generalQuota) continue;
|
||||
quotaBucket = 'general';
|
||||
occupiedGeneral.set(key, (occupiedGeneral.get(key) || 0) + 1);
|
||||
}
|
||||
occupied.set(key, (occupied.get(key) || 0) + 1);
|
||||
created.push({
|
||||
id: uid('placement'), kind: 'placement', examId, userId: candidate.preference.userId, schoolId: choice.schoolId,
|
||||
status: 'school_review', createdAt: nowIso(), updatedAt: nowIso(), payload: {
|
||||
round, categoryCode: category.code, categoryName: category.name, preferenceOrder: index + 1,
|
||||
totalScore: candidate.score, quotaBucket, schoolDecisionNote: '', withdrawalReason: '', withdrawalReviewNote: ''
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
export function remainingPlanQuota(db, plan) {
|
||||
return (plan.payload?.categories || []).map(category => {
|
||||
const used = admissionRecords(db, 'placement', plan.examId).filter(item => item.schoolId === plan.schoolId && item.payload?.categoryCode === category.code && item.status !== 'withdrawn').length;
|
||||
return { ...category, used, remaining: Math.max(0, Number(category.quota || 0) - used) };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user