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) { 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 (!candidateEligibleForCategory(candidate.profile, category)) 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, featureScore: Number(db.registrations.find(item => item.examId === examId && item.userId === candidate.preference.userId)?.featureScore || 0), specialtyQualification: resolveProfileSpecialty(candidate.profile), 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) }; }); } import { candidateEligibleForCategory, resolveProfileSpecialty } from '../data/specialty-types.mjs';