PageSourceSearch

https://tvmc-dangky.web.app/js/at-school-auto-selection.js?v=20260817-4

js tvmc-dangky.web.app collected 2026-10-03 12:38:34 UTC 17,966 bytes, 385 lines download raw bytes

1/* Bước đề xuất nhúng vào luồng đăng ký tại trường.
2   Phần quyết định dùng trực tiếp bộ luật của trang de-xuat-xet-tuyen.html để
3   hai nơi không thể tính ra hai phương án khác nhau từ cùng một bộ điểm. */
4import {
5    MON_TRU_KHOI_TB,
6    SO_MON_LOP12,
7    MON_THI_BAT_BUOC,
8    MON_THI_TU_CHON,
9    soDiem,
10    hienDiem,
11    layMonPT2,
12    timToHopPT2,
13    soMonXet,
14    tinhTbLop12,
15    tinhTbThiTN,
16    xepDeXuat
17} from './de-xuat-xet-tuyen.js?v=20260817-3';
18import { TEN_PHUONG_THUC } from './to-hop-xet-tuyen.js?v=20260817-2';
19
20const escapeHtml = (value) => String(value ?? '')
21    .replace(/&/g, '&')
22    .replace(/</g, '&lt;')
23    .replace(/>/g, '&gt;')
24    .replace(/"/g, '&quot;')
25    .replace(/'/g, '&#039;');
26
27const state = {
28    level: '',
29    skipPt2: false
30};
31
32const getLevel = () => document.getElementById('nganh')?.selectedOptions?.[0]?.dataset.trinhdo || '';
33
34const clearAppliedSelection = () => {
35    if (!document.body.classList.contains('registration-at-school')) return;
36    const method = document.getElementById('phuongthuc');
37    const combination = document.getElementById('tohop');
38    if (method) method.value = '';
39    if (combination) combination.value = '';
40    [1, 2, 3].forEach((index) => {
41        const input = document.getElementById(`diem_m${index}`);
42        if (input) input.value = '';
43    });
44};
45
46const hideResult = (container) => {
47    const result = container.querySelector('#at_school_rec_result');
48    if (!result) return;
49    result.hidden = true;
50    result.innerHTML = '';
51};
52
53const invalidateResult = (container) => {
54    hideResult(container);
55    clearAppliedSelection();
56};
57
58const renderShell = (container) => {
59    container.innerHTML = `
60        <div class="at-school-auto-head">
61            <i class="fa-solid fa-wand-magic-sparkles"></i>
62            <div>
63                <h3 id="tai-truong-auto-title">Đề xuất phương thức và tổ hợp</h3>
64                <p>Dùng cùng quy trình và cách tính của mục “Đề xuất phương thức &amp; tổ hợp”.</p>
65            </div>
66        </div>
67
68        <section class="at-school-rec-step" id="at_school_rec_pt2">
69            <h4><i class="fa-solid fa-book"></i> 1. Lớp 12 học những môn nào?</h4>
70            <p class="at-school-rec-sub">Chọn những môn có học và có điểm trong học bạ lớp 12, rồi nhập điểm trung bình cả năm của môn đó.</p>
71            <div id="at_school_rec_subjects"></div>
72            <label class="at-school-rec-skip">
73                <input type="checkbox" id="at_school_rec_skip_pt2">
74                <span>Không học nhóm môn này — bỏ qua xét học bạ PT2</span>
75            </label>
76            <div class="at-school-rec-note" id="at_school_rec_pt2_note" hidden></div>
77            <button type="button" class="at-school-rec-secondary" id="at_school_rec_open_pt3">
78                <i class="fa-solid fa-plus"></i> Tính thêm phương thức kết hợp PT3
79            </button>
80        </section>
81
82        <section class="at-school-rec-step" id="at_school_rec_pt3" hidden>
83            <h4><i class="fa-solid fa-calculator"></i> 2. Điểm cho phương thức kết hợp và điểm thi</h4>
84            <p class="at-school-rec-sub">PT3 dùng điểm trung bình cả năm lớp 12, <b class="at-school-rec-danger">không tính ${escapeHtml(MON_TRU_KHOI_TB)}</b>.</p>
85            <label for="at_school_rec_subject_count">Lớp 12 có bao nhiêu môn có điểm sau khi trừ GDQP-AN?</label>
86            <select class="form-control" id="at_school_rec_subject_count">
87                <option value="">-- Chọn số môn --</option>
88                ${SO_MON_LOP12.map(number => `<option value="${number}">${number} môn</option>`).join('')}
89            </select>
90            <div id="at_school_rec_grade12_scores"></div>
91            <div class="at-school-rec-average">Trung bình cả năm lớp 12: <b id="at_school_rec_grade12_average">chưa đủ điểm</b></div>
92
93            <p class="at-school-rec-exam-title"><b>Điểm thi tốt nghiệp THPT</b> — đúng 4 môn: Toán, Ngữ văn và hai môn tự chọn.</p>
94            <div id="at_school_rec_exam_scores"></div>
95            <div class="at-school-rec-average">Trung bình 4 môn thi tốt nghiệp: <b id="at_school_rec_exam_average">
95chưa đủ 4 môn</b></div>
96        </section>
97
98        <button type="button" class="at-school-auto-action" id="at_school_rec_calculate">
99            <i class="fa-solid fa-ranking-star"></i> Xem đề xuất và áp dụng
100        </button>
101        <div class="at-school-auto-result" id="at_school_rec_result" hidden aria-live="polite"></div>`;
102};
103
104const getSelectedPt2Subjects = (container) => state.skipPt2
105    ? []
106    : [...container.querySelectorAll('.at-school-rec-subject:checked')].map(input => input.value);
107
108const getTranscriptScores = (container) => {
109    const scores = {};
110    if (state.skipPt2) return scores;
111    container.querySelectorAll('.at-school-rec-transcript-score').forEach((input) => {
112        if (!input.disabled && soDiem(input.value) !== null) scores[input.dataset.subject] = soDiem(input.value);
113    });
114    return scores;
115};
116
117const updatePt2Note = (container) => {
118    const note = container.querySelector('#at_school_rec_pt2_note');
119    if (!note) return;
120    if (state.skipPt2) {
121        note.hidden = false;
122        note.className = 'at-school-rec-note is-warning';
123        note.innerHTML = `<b>Đã bỏ qua phương thức xét học bạ PT2.</b><br>
124            Tiếp tục nhập điểm ở phần phương thức kết hợp PT3 bên dưới.`;
125        return;
126    }
127
128    const selected = getSelectedPt2Subjects(container);
129    if (!selected.length) {
130        note.hidden = true;
131        return;
132    }
133    const combinations = timToHopPT2(state.level, selected);
134    note.hidden = false;
135    note.className = `at-school-rec-note ${combinations.length ? 'is-success' : 'is-warning'}`;
136    note.innerHTML = combinations.length
137        ? `<b>Dùng được phương thức xét học bạ PT2.</b><br>
138           Ghép được ${combinations.length} tổ hợp: ${combinations.map(item => escapeHtml(item.value)).join(' · ')}.`
139        : `<b>Chưa ghép được tổ hợp PT2.</b><br>
140           Bậc này cần đủ ${soMonXet(state.level)} môn trong cùng một tổ hợp; vẫn có thể xét bằng PT3.`;
141};
142
143const renderPt2Subjects = (container) => {
144    const host = container.querySelector('#at_school_rec_subjects');
145    if (!host || !state.level) return;
146    host.innerHTML = layMonPT2(state.level).map((subject) => `
147        <label class="at-school-rec-subject-row">
148            <input type="checkbox" class="at-school-rec-subject" value="${escapeHtml(subject)}"${subject === 'Toán' ? ' checked' : ''}>
149            <span>${escapeHtml(subject)}</span>
150            <input type="text" inputmode="decimal" autocomplete="off"
151                class="at-school-rec-transcript-score" data-subject="${escapeHtml(subject)}"
152                placeholder="Điểm TB cả năm"${subject === 'Toán' ? '' : ' disabled'}>
153        </label>`).join('')
154        + `<div class="at-school-rec-note is-info">Mọi tổ hợp đều có môn Toán nên Toán được chọn sẵn.</div>`;
155    updatePt2Note(container);
156};
157
158const updateGrade12Average = (container) => {
159    const scores = [...container.querySelectorAll('.at-school-rec-grade12-score')].map(input => input.value);
160    const average = tinhTbLop12(scores);
161    const output = container.querySelector('#at_school_rec_grade12_average');
162    if (output) output.textContent = average === null ? 'chưa đủ điểm' : hienDiem(average);
163};
164
165const renderGrade12Scores = (container) => {
166    const host = container.querySelector('#at_school_rec_grade12_scores');
167    const count = Number(container.querySelector('#at_school_rec_subject_count')?.value) || 0;
168    if (!host) return;
169    if (!count) {
170        host.innerHTML = '';
171        updateGrade12Average(container);
172        return;
173    }
174
175    const transcript = getTranscriptScores(container);
176    const knownSubjects = ['Toán', ...Object.keys(transcript).filter(subject => subject !== 'Toán')];
177    host.innerHTML = Array.from({ length: count }, (_, index) => {
178        const subject = knownSubjects[index] || '';
179        const value = subject && transcript[subject] !== undefined ? hienDiem(transcript[subject]) : '';
180        return `<div class="at-school-rec-score-row">
181            <span>${escapeHtml(subject || `Môn thứ ${index + 1}`)}</span>
182            <input type="text" inputmode="decimal" autocomplete="off"
183                class="at-school-rec-grade12-score"${index === 0 ? ' id="at_school_rec_grade12_math"' : ''}
184                value="${escapeHtml(value)}" placeholder="Điểm TB cả năm">
185        </div>`;
186    }).join('') + `<div class="at-school-rec-note is-warning"><b>Nhập đủ ${count} môn, không nhập ${escapeHtml(MON_TRU_KHOI_TB)}.</b></div>`;
187    updateGrade12Average(container);
188};
189
190const updateExamAverage = (container) => {
191    const scores = [...container.querySelectorAll('.at-school-rec-exam-score')].map(input => input.value);
192    const average = tinhTbThiTN(scores);
193    const output = container.querySelector('#at_school_rec_exam_average');
194    if (output) output.textContent = average === null ? 'chưa đủ 4 môn' : hienDiem(average);
195};
196
197const renderExamScores = (container) => {
198    const host = container.querySelector('#at_school_rec_exam_scores');
199    if (!host || host.children.length) return;
200    host.innerHTML = MON_THI_BAT_BUOC.map(subject => `
201        <div class="at-school-rec-score-row">
202            <span>${escapeHtml(subject)}</span>
203            <input type="text" inputmode="decimal" autocomplete="off"
204                class="at-school-rec-exam-score" data-subject="${escapeHtml(subject)}" placeholder="Điểm thi">
205        </div>`).join('')
206        + [1, 2].map(index => `
207        <div class="at-school-rec-score-row has-select">
208            <select class="at-school-rec-exam-subject" data-slot="${index}">
209                <option value="">-- Môn tự chọn ${index} --</option>
210                ${MON_THI_TU_CHON.map(subject => `<option value="${escapeHtml(subject)}">${escapeHtml(subject)}</option>`).join('')}
211            </select>
212            <input type="text" inputmode="decimal" autocomplete="off"
213                class="at-school-rec-exam-score" data-slot="${index}" placeholder="Điểm thi">
214        </div>`).join('')
215        + `<div class="at-school-rec-note is-info">Chưa thi thì để trống cả bốn ô; hệ thống vẫn xét các phương án không cần điểm thi.</div>`;
216    updateExamAverage(container);
217};
218
219const openPt3 = (container) => {
220    const section = container.querySelector('#at_school_rec_pt3');
221    if (section) section.hidden = false;
222    renderExamScores(container);
223};
224
225const setSkipPt2 = (container, skip) => {
226    state.skipPt2 = Boolean(skip);
227    const host = container.querySelector('#at_school_rec_subjects');
228    host?.classList.toggle('is-disabled', state.skipPt2);
229    host?.querySelectorAll('.at-school-rec-subject').forEach((input) => {
230        input.disabled = state.skipPt2;
231    });
232    host?.querySelectorAll('.at-school-rec-transcript-score').forEach((input) => {
233        const checked = host.querySelector(`.at-school-rec-subject[value="${CSS.escape(input.dataset.subject || '')}"]`)?.checked;
234        input.disabled = state.skipPt2 || !checked;
235    });
236    updatePt2Note(container);
237    if (state.skipPt2) openPt3(container);
238};
239
240const collectData = (container) => {
241    const transcript = getTranscriptScores(container);
242    if (transcript.Toán === undefined) {
243        const math = soDiem(container.querySelector('#at_school_rec_grade12_math')?.value);
244        if (math !== null) transcript.Toán = math;
245    }
246
247    const exam = {};
248    container.querySelectorAll('.at-school-rec-exam-score').forEach((input) => {
249        const subject = input.dataset.subject
250            || container.querySelector(`.at-school-rec-exam-subject[data-slot="${input.dataset.slot || ''}"]`)?.value
251            || '';
252        const score = soDiem(input.value);
253        if (subject && score !== null) exam[subject] = score;
254    });
255
256    const grade12Scores = [...container.querySelectorAll('.at-school-rec-grade12-score')].map(input => input.value);
257    const examScores = [...container.querySelectorAll('.at-school-rec-exam-score')].map(input => input.value);
258    return {
259        hocBa: transcript,
260        tbLop12: tinhTbLop12(grade12Scores),
261        thiTN: exam,
262        tbThiTN: tinhTbThiTN(examScores)
263    };
264};
265
266const applyRecommendation = (top) => {
267    const method = document.getElementById('phuongthuc');
268    const combination = document.getElementById('tohop');
269    if (!method || !combination || !top) return false;
270
271    method.value = top.pt;
272    window.handleNganhVaPhuongThucChange?.();
273    if (![...combination.options].some(option => option.value === top.toHop)) return false;
274    combination.value = top.toHop;
275    window.handleToHopChange?.();
276    top.chiTiet.forEach((item, index) => {
277        const input = document.getElementById(`diem_m${index + 1}`);
278        if (input) input.value = hienDiem(item.diem);
279    });
280    window.updateScoreLive?.();
281    return true;
282};
283
284const renderRecommendation = (container) => {
285    const result = container.querySelector('#at_school_rec_result');
286    if (!result || !state.level) return;
287    if (!timToHopPT2(state.level, getSelectedPt2Subjects(container)).length) openPt3(container);
288
289    const { du, thieu } = xepDeXuat(state.level, collectData(container));
290    result.hidden = false;
291    if (!du.length) {
292        clearAppliedSelection();
293        const missing = [...new Set(thieu.flatMap(item => item.thieu))];
294        result.className = 'at-school-auto-result is-error';
295        result.innerHTML = `<b>Chưa tính được phương án vì còn thiếu điểm.</b>
296            ${missing.length ? `<br><small>Thiếu: ${escapeHtml(missing.join(', '))}</small>` : ''}`;
297        return;
298    }
299
300    const top = du[0];
301    if (!applyRecommendation(top)) {
302        result.className = 'at-school-auto-result is-error';
303        result.innerHTML = '<b>Không áp dụng được tổ hợp đề xuất vào hồ sơ.</b>';
304        return;
305    }
306
307    const remaining = du.slice(1);
308    result.className = 'at-school-auto-result';
309    result.innerHTML = `
310        <div class="at-school-rec-result-head">
311            <div>
312                <span>Nên chọn</span>
313                <b>${escapeHtml(top.pt)} — ${escapeHtml(TEN_PHUONG_THUC[top.pt] || top.pt)}</b>
314                <strong>${escapeHtml(top.toHop)}</strong>
315            </div>
316            <em>${hienDiem(top.tongCacMon)}</em>
317        </div>
318        <div class="at-school-rec-result-detail">${top.chiTiet.map(item => `${escapeHtml(item.mon)}: <b>${hienDiem(item.diem)}</b>`).join(' · ')}
319            <br>Trung bình: <b>${hienDiem(top.diemTb)}</b></div>
320        <p><i class="fa-solid fa-circle-check"></i> Đã áp dụng phương thức, tổ hợp và điểm vào hồ sơ tại trường.</p>
321        ${remaining.length ? `<details><summary>Xem ${remaining.length} phương án đủ điểm còn lại</summary>
322            ${remaining.map(item => `<div class="at-school-rec-other"><b>${escapeHtml(item.pt)}</b> — ${escapeHtml(item.toHop)}: ${hienDiem(item.tongCacMon)}</div>`).join('')}
323        </details>` : ''}`;
324};
325
326const resetForMajor = (container) => {
327    state.level = getLevel();
328    state.skipPt2 = false;
329    renderShell(container);
330    renderPt2Subjects(container);
331    clearAppliedSelection();
332};
333
334const mountAtSchoolRecommendation = () => {
335    const container = document.getElementById('tai-truong-auto-xet-tuyen');
336    if (!container) return;
337    resetForMajor(container);
338
339    container.addEventListener('input', (event) => {
340        invalidateResult(container);
341        if (event.target.matches('.at-school-rec-transcript-score')) updatePt2Note(container);
342        if (event.target.matches('.at-school-rec-grade12-score')) updateGrade12Average(container);
343        if (event.target.matches('.at-school-rec-exam-score')) updateExamAverage(container);
344    });
345
346    container.addEventListener('change', (event) => {
347        invalidateResult(container);
348        if (event.target.matches('.at-school-rec-subject')) {
349            const score = [...container.querySelectorAll('.at-school-rec-transcript-score')]
350                .find(input => input.dataset.subject === event.target.value);
351            if (score) {
352                score.disabled = !event.target.checked;
353                if (!event.target.checked) score.value = '';
354            }
355            updatePt2Note(container);
356        } else if (event.target.id === 'at_school_rec_skip_pt2') {
357            setSkipPt2(container, event.target.checked);
358        } else if (event.target.id === 'at_school_rec_subject_count') {
359            renderGrade12Scores(container);
360        } else if (event.target.matches('.at-school-rec-exam-subject')) {
361            updateExamAverage(container);
362        }
363    });
364
365    container.addEventListener('click', (event) => {
366        const button = event.target.closest('button');
367        if (!button) return;
368        if (button.id === 'at_school_rec_open_pt3') openPt3(container);
369        if (button.id === 'at_school_rec_calculate') renderRecommendation(container);
370    });
371
372    document.getElementById('nganh')?.addEventListener('change', () => resetForMajor(container));
373
374    window.syncAtSchoolAutoSelectionMode = (enabled) => {
375        hideResult(container);
376        if (enabled) resetForMajor(container);
377    };
378
379    if (document.body.classList.contains('registration-at-school')) resetForMajor(container);
380};
381
382if (typeof document !== 'undefined') {
383    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mountAtSchoolRecommendation);
384    else mountAtSchoolRecommendation();
385}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.