var BASEURL = `${window.location.protocol}//${window.location.hostname}`; var currentUrl = window.location.href; (function (webapi, $) { function safeAjax(opts) { var d = $.Deferred(); shell.getTokenDeferred().done(function (token) { if (!opts.headers) { $.extend(opts, { headers: { "__RequestVerificationToken": token } }); } else { opts.headers["__RequestVerificationToken"] = token; } $.ajax(opts).done(function (data, textStatus, jqXHR) { validateLoginSession(data, textStatus, jqXHR, function (resolved) { if (opts && (opts.returnXHR || opts.returnXhr || opts.returnJqXHR)) { d.resolve(jqXHR); } else { d.resolve(resolved); } }); }).fail(d.reject); }).fail(function () { d.rejectWith(this, arguments); }); return d.promise(); } webapi.safeAjax = safeAjax; })(window.webapi = window.webapi || {}, jQuery); var notificationMsg = (function () { var $el = $('#processingMsg'), _msg = 'Processing...', _stack = 0, _end; return { show: function (msg) { $el.text(msg || _msg); if (_stack === 0) { clearTimeout(_end); $el.show(); } _stack++; }, hide: function () { _stack--; if (_stack <= 0) { _stack = 0; clearTimeout(_end); _end = setTimeout(function () { $el.hide(); }, 50); } } }; })(); var urlParams = new URLSearchParams(window.location.search), modeId = !!urlParams.get("advanced"), studentId = resolveStudentId(urlParams.get("studentid") || urlParams.get("contactid")), btfhCurrentProgram = "", btfhCurrentProgramVersion = "", btfhCurrentProgramVersionText = "", btfhMaxCreditValue = 120, opportunityId = null, ContactData = [], CurrentSAQAID = "", btfhTargetProgamVersion = "", btfhTargetProgamVersionText = "", btfhTargetProgamVersionYear = "", btfhCurrentIntake = "", currentAcademicPeriod = [], courseEquivalencyDict = [], programVersionsDict = [], intakeDict = [], outStandingModules = [], qualProgData = [], courseHistoryData = null, fullCourseHistoryData = null, MonthlyPriceList = "", UpfrontPriceList = ""; var existingContactPaymentValue = null; var existingOppPaymentValue = null; var qualPlannerData = null; var futurePlannerData = null; function resolveStudentId(candidate) { const c = (candidate || "").toString().trim(); if (c && c !== "{{ user.id }}") return c; if (typeof window !== "undefined") { const fromWin = window.__currentStudentId || window.studentId || window.StudentId; if (fromWin && fromWin !== "{{ user.id }}") return fromWin; } if (typeof user !== "undefined" && user && user.id && user.id !== "{{ user.id }}") return user.id; return ""; } const FORCE_MONTHLY = true; const MONTHLY_OPTION_VALUE = 757490000; const EXCLUDE_S_CODES = true; let _progressModal = null; function showProgressModal(title = "Updating your enrolment", subtitle = "Please wait while we apply your changes...") { const html = ``; document.body.insertAdjacentHTML('beforeend', html); _progressModal = document.getElementById('progress-modal'); } let _plannerMinOfferingYear = null; function _initPlannerMinOfferingYear(planner) { if (_plannerMinOfferingYear != null) return; const years = (planner?.results || []) .map(r => Number(r.YearOfOffering)) .filter(y => Number.isFinite(y) && y >= 1900); _plannerMinOfferingYear = years.length ? Math.min(...years) : null; } function _deriveYearLocal(obj) { const yField = Number(obj && obj.Year); if (Number.isFinite(yField) && yField > 0) return yField; const yUnd = Number(obj && obj.YearOfUnderTaking); if (Number.isFinite(yUnd) && yUnd > 0) return yUnd; const pc = String((obj && (obj.ProductCode || obj.ProductNumber || obj.ModuleCode)) || ""); const m = pc.match(/(\d)(?=-[A-Za-z])/); if (m) return Number(m[1]); const base = pc.split("-")[0]; if (typeof getModuleYear === "function") { const yFromBase = getModuleYear(base); if (Number.isFinite(yFromBase) && yFromBase > 0) return yFromBase; } return 0; } function deriveYear(obj) { if (typeof window !== "undefined" && typeof window.deriveYear === "function" && window.deriveYear !== deriveYear) { return window.deriveYear(obj); } return _deriveYearLocal(obj); } function getYearFromCourse(c) { const full = (c && c.mshied_CourseId && c.mshied_CourseId.mshied_coursenumber) ? String(c.mshied_CourseId.mshied_coursenumber).trim() : ""; if (!full) return 0; const base = full.split("-")[0]; if (typeof getModuleYear === "function") { const y = getModuleYear(base); if (Number.isFinite(y) && y > 0 && y < 50) return y; } const m = base.match(/(\d+)[^\d]*$/); if (m) { const y2 = parseInt(m[1], 10); if (Number.isFinite(y2) && y2 > 0 && y2 < 50) return y2; } return 0; } function getIntakeYearFromCode(code) { const s = String(code || ""); const m = s.match(/-(\d{2})-[A-Za-z]$/); if (!m) return null; const yy = Number(m[1]); if (!Number.isFinite(yy)) return null; return yy >= 50 ? 1900 + yy : 2000 + yy; } function getIntakeYearForModule(obj) { if (!obj) return null; const y = Number(obj.YearOfOffering); if (Number.isFinite(y) && y >= 1900) return y; const yu = Number(obj.YearOfUnderTaking); if (Number.isFinite(yu) && yu >= 1900) return yu; return getIntakeYearFromCode(obj.ProductCode || obj.ProductNumber || obj.ModuleCode); } function getSelectedIntakeYear() { try { let yd = null, yt = null; if (intakeDict && Array.isArray(intakeDict.results) && intakeDict.results.length) { const selId = typeof $ === "function" ? $("#btfh_intake").val() : null; if (selId) { const rec = intakeDict.results.find(r => (r.Id && r.Id === selId) || (r.IntakeId && r.IntakeId === selId) || (r.Intake && r.Intake === selId) ); if (rec) { const y = Number(rec.Year); if (Number.isFinite(y) && y >= 1900) yd = y; } } } if (typeof $ === "function") { const txt = $("#btfh_intake option:selected").text() || ""; const m = txt.match(/\b(20\d{2})\b/); if (m) { const y2 = Number(m[1]); if (Number.isFinite(y2) && y2 >= 1900) yt = y2; } } if (Number.isFinite(yd) && Number.isFinite(yt)) return Math.max(yd, yt); if (Number.isFinite(yt)) return yt; if (Number.isFinite(yd)) return yd; } catch (e) { } return null; } function isIntakeYearAllowedForSelection(intakeYear, allowedYears, selectedYear) { if (!Number.isFinite(intakeYear) || intakeYear < 1900) return true; if (Number.isFinite(selectedYear) && selectedYear >= 1900) { return intakeYear === selectedYear; } if (allowedYears && allowedYears.size && typeof allowedYears.has === "function") { return allowedYears.has(intakeYear); } return true; } function addModuleToQualProg(qualProgData, d) { if (d.ModuleStatus === "FAILED" || d.ModuleStatus === "PASSED") { qualProgData.push(d); } else if (!qualProgData.some(m => m.ModuleCode === d.ModuleCode)) { qualProgData.push(d); } } function isSCodeModule(mod) { const cls = (mod.ModuleClass || "").toUpperCase().trim(); if (cls === "FUNDAMENTAL") return false; const c = (mod.ModuleCode || "").toUpperCase().trim(); const b = c.split("-")[0]; const m1 = /([A-Z])(\d+)$/.exec(b); if (m1 && m1[1] === "S") return true; const p = (mod.ProductCode || mod.ProductNumber || "").toUpperCase(); const segments = p.split("-"); for (const seg of segments) { const m = /([A-Z])(\d+)$/.exec(seg); if (m && m[1] === "S") return true; } return false; } function isCoreLike(m) { const t = (m.Type || m.ModuleType || "").toString().trim(); const cls = (m.ModuleClass || "").toString().trim().toUpperCase(); if ( t === "" || t === "Core" || t === "None" || t === "Elective" || t === "Elective Group" || t === "Optional" || t === null ) return true; if (cls === "FUNDAMENTAL") return true; return false; } function isAdminModule(m) { const t = (m && (m.Type || m.ModuleType) || "").toString().toUpperCase().trim(); const cls = (m && m.ModuleClass || "").toString().toUpperCase().trim(); const code = (m && (m.ModuleCode || m.ProductCode || m.ProductNumber) || "").toString().toUpperCase().trim(); const name = (m && (m.Name || m.ProductName) || "").toString().toUpperCase().trim(); const isAdminFeeText = s => (!!s && ((s.includes("ADMIN") && s.includes("FEE")) || (s.includes("ADMISSION") && s.includes("FEE")))); if (isAdminFeeText(t) || isAdminFeeText(cls) || isAdminFeeText(name)) return true; if (code.startsWith("ADMIN")) return true; if (code.includes("ADMIN") && code.includes("FEE")) return true; return false; } function isAdminModuleLocal(m) { return isAdminModule(m); } function isInProgressLikeStatus(status) { const u = (status || "").toUpperCase(); if (!u) return false; if (["REGISTERED", "IN PROGRESS", "CREDIT EXEMPTION"].includes(u)) return true; if (u.startsWith("SUP") || u.includes("SPECIAL ASSESSMENT")) return true; return false; } function getBaseModuleCode(mc) { return (mc || "").split("-")[0].toUpperCase().trim(); } function computeCoreTotalsFromPlanner(planner) { const totals = {}; (planner.results || []).forEach(m => { if (typeof EXCLUDE_S_CODES !== "undefined" && EXCLUDE_S_CODES && isSCodeModule(m)) return; const yr = deriveYear(m); const credits = Number(m.Credits) || 0; const isCore = m.Type === "Core" || m.ModuleClass === "Fundamental"; if (isCore && m.Repeat !== "true") { totals[yr] = (totals[yr] || 0) + credits; } }); return totals; } function computeRequiredBreakdownFromPlanner(planner) { const breakdown = {}; (planner.results || []).forEach(m => { if (typeof EXCLUDE_S_CODES !== "undefined" && EXCLUDE_S_CODES && isSCodeModule(m)) return; const yr = deriveYear(m); const credits = Number(m.Credits) || 0; if (!credits) return; if (m.Repeat === "true") return; if (m.Type === "Optional") return; const isCore = m.Type === "Core" || m.ModuleClass === "Fundamental"; const isElective = m.Elective === "true" || m.Type === "Elective" || m.Type === "Elective Group"; let weightedCredits = 0; if (isCore) weightedCredits = credits; else if (isElective) weightedCredits = credits * 0.5; else return; if (!breakdown[yr]) breakdown[yr] = []; const fullCode = m.ProductCode || m.ModuleCode || ""; const root = fullCode.split("-")[0] || fullCode; breakdown[yr].push({ code: fullCode, root, credits, weightedCredits, isCore, isElective, name: m.Name || m.ProductName || "" }); }); return breakdown; } function updateYearlyCredits(y, m, yearlyTotalCredits, yearlyMaxCredits, qualPlannerData, programVersionsDict) { const c = parseInt(m.Credits) || 0; const t = m.Type; const isIncluded = t === "Core" || t === "None" || t == null || t === "" || t === "Elective" || t === "Elective Group" || t === "Optional"; if (isIncluded && m.Repeat !== "true") { const max = getMaxCredits(y, qualPlannerData, programVersionsDict) || 0; yearlyTotalCredits[y] = max || yearlyTotalCredits[y] || 0; yearlyMaxCredits[y] = max || c || 0; } } function _processCourseHistoryLocal(courseHistoryData, qualPlannerData, qualProgData, yearlyCoreCredits) { (courseHistoryData?.value || []).forEach(cr => { const y = getYearFromCourse(cr); const cid = cr.mshied_CourseId; if (!cid) return; const prod = cr.bt_Product; const spd = prod?.["_msdyn_sharedproductdetailid_value@OData.Community.Display.V1.FormattedValue"] || "0"; const blk = parseInt(spd.slice(-2, -1) || "0", 10) || 0; const baseModuleData = { Id: "", Name: cr.mshied_name, Repeat: (window.getRepeat ? window.getRepeat(cid.mshied_coursenumber, qualPlannerData) : false).toString(), Credits: cid.bt_creditvalue || 0, YearOfOffering: cr.bt_year || 0, YearOfUnderTaking: cr["bt_yearofundertaking@OData.Community.Display.V1.FormattedValue"], StartsInBlock: `Block ${blk}`, ModulePeriod: blk, ProductNumber: spd, ProductId: prod ? prod.productid : null, ProductCode: cid.mshied_code || null, ProductName: cid["_bt_product_value@OData.Community.Display.V1.FormattedValue"] || "", Year: y, Elective: (window.getElective ? window.getElective(cid.mshied_coursenumber, qualPlannerData) : false).toString(), Type: window.getModuleType ? window.getModuleType(cid.mshied_coursenumber, qualPlannerData) : null, ModuleType: window.getModuleType ? window.getModuleType(cid.mshied_coursenumber, qualPlannerData) : null, ModuleCode: cid.mshied_coursenumber, ModuleClass: window.getModuleClass ? window.getModuleClass(cid.mshied_coursenumber, qualPlannerData) : null, ModuleStatus: (cr["bt_modulestatus@OData.Community.Display.V1.FormattedValue"] || "").toUpperCase() || null }; if ( (baseModuleData.Type === "Core" || baseModuleData.Type === "None" || baseModuleData.Type === null || baseModuleData.Type === "" || baseModuleData.Type === "Elective" || baseModuleData.Type === "Optional") && baseModuleData.Repeat !== "true" ) { if (baseModuleData.ModuleClass !== "Fundamental") { yearlyCoreCredits[y] = (yearlyCoreCredits[y] || 0) + (baseModuleData.Credits || 0); } } const mc = cid.mshied_coursenumber; const all = (typeof getEquivalentsHelper === "function" ? getEquivalentsHelper(mc) : (typeof getEquivalents === "function" ? getEquivalents(mc) : [mc])) || [mc]; all.forEach(eC => { const eNorm = (typeof getNormalizedRootCode === "function") ? getNormalizedRootCode(eC) : eC; (qualPlannerData.results || []).forEach(pM => { const tRaw = pM && (pM.ModuleCode || pM.ProductCode || pM.ProductNumber) || ""; const tNorm = (typeof getNormalizedRootCode === "function") ? getNormalizedRootCode(tRaw) : String(tRaw || "").split("-")[0]; if (tNorm === eNorm) addModuleToQualProg(qualProgData, { ...baseModuleData, ModuleCode: eC }); }); }); }); } function processCourseHistory(courseHistoryData, qualPlannerData, qualProgData, yearlyCoreCredits) { if (typeof window !== "undefined" && typeof window.processCourseHistory === "function" && window.processCourseHistory !== processCourseHistory) { return window.processCourseHistory(courseHistoryData, qualPlannerData, qualProgData, yearlyCoreCredits); } return _processCourseHistoryLocal(courseHistoryData, qualPlannerData, qualProgData, yearlyCoreCredits); } function setProgress(pct, label) { const bar = document.getElementById('progress-bar'); if (bar) bar.style.width = `${Math.max(0, Math.min(100, pct))}%`; if (label) { const lab = document.getElementById('progress-label'); if (lab) lab.textContent = label; } } function hideProgressModal() { if (_progressModal) { _progressModal.remove(); _progressModal = null; } } let _lastAppliedPriceLevelKey = ""; async function checkAndUpdatePriceLevelIfNeeded() { const selectedIntakeId = (typeof $ === "function") ? $("#btfh_intake option:selected").val() : null; const effectiveIntakeId = selectedIntakeId || btfhCurrentIntake; await ensurePriceLists(btfhCurrentProgramVersion, effectiveIntakeId); const selectedPaymentValue = MONTHLY_OPTION_VALUE; const selectedPriceLevelId = MonthlyPriceList; if (!selectedPriceLevelId) { console.warn("Missing monthly price list."); return; } const applyKey = `${effectiveIntakeId || ""}|${selectedPaymentValue || ""}|${(selectedPriceLevelId || "").toString().toLowerCase()}`; if (applyKey && applyKey === _lastAppliedPriceLevelKey) return; try { const currentPayment = (typeof existingContactPaymentValue === "number") ? existingContactPaymentValue : (ContactData && typeof ContactData.edv_paymentpreference === "number" ? ContactData.edv_paymentpreference : null); const currentPriceLevelId = ContactData ? (ContactData._defaultpricelevelid_value || ContactData.defaultpricelevelid || null) : null; const norm = v => (v || "").toString().replace(/[{}]/g, "").trim().toLowerCase(); const shouldPatchContact = (currentPayment !== selectedPaymentValue) || (norm(currentPriceLevelId) !== norm(selectedPriceLevelId)); if (shouldPatchContact) { updateContactPriceLevel(studentId, selectedPriceLevelId, selectedPaymentValue); } } catch (e) { updateContactPriceLevel(studentId, selectedPriceLevelId, selectedPaymentValue); } if (!opportunityId) { opportunityId = await checkForExistingOpportunity(studentId); } if (opportunityId) { try { const norm = v => (v || "").toString().replace(/[{}]/g, "").trim().toLowerCase(); const cur = await webapi.safeAjax({ type: "GET", url: `https://www.vossie.net/_api/opportunities(${opportunityId})?$select=edv_pricelistselection,_pricelevelid_value`, dataType: "json" }); const curPay = (cur && typeof cur.edv_pricelistselection === "number") ? cur.edv_pricelistselection : null; const curPl = cur && cur._pricelevelid_value; const shouldPatchOpp = (curPay !== selectedPaymentValue) || (norm(curPl) !== norm(selectedPriceLevelId)); if (shouldPatchOpp) { await webapi.safeAjax({ type: "PATCH", url: `https://www.vossie.net/_api/opportunities(${opportunityId})`, contentType: "application/json", data: JSON.stringify({ "pricelevelid@odata.bind": `/pricelevels(${selectedPriceLevelId})`, "edv_pricelistselection": selectedPaymentValue }) }); } } catch (e) { await webapi.safeAjax({ type: "PATCH", url: `https://www.vossie.net/_api/opportunities(${opportunityId})`, contentType: "application/json", data: JSON.stringify({ "pricelevelid@odata.bind": `/pricelevels(${selectedPriceLevelId})`, "edv_pricelistselection": selectedPaymentValue }) }); } } _lastAppliedPriceLevelKey = applyKey; } function setStep(n) { const items = document.querySelectorAll('#progress-steps li'); items.forEach((li, idx) => { li.classList.remove('active', 'completed'); if (idx < n - 1) li.classList.add('completed'); if (idx === n - 1) li.classList.add('active'); }); } function hasElectives() { return (outStandingModules?.elective || []).length > 0; } function selectedElectiveCount() { return document.querySelectorAll('input.elective-cb[type="checkbox"]:checked').length; } function updateElectiveStepState() { if (!hasElectives()) { setStep(3); return; } if (selectedElectiveCount() > 0) { setStep(3); } else { setStep(2); } } let equivalencyMap = new Map(); function buildEquivalencyMap(list) { const m = new Map(); (list || []).forEach(({ primarycode, equivalentcodes }) => { const p = String(primarycode || '').toUpperCase(); if (!p) return; if (!m.has(p)) m.set(p, new Set()); (equivalentcodes || []).forEach(eq => { const e = String(eq || '').toUpperCase(); if (!e) return; m.get(p).add(e); if (!m.has(e)) m.set(e, new Set()); m.get(e).add(p); }); }); return m; } function getEquivalents(code) { const key = String(code || '').toUpperCase(); const s = new Set([key]); const eq = equivalencyMap.get(key); if (eq) eq.forEach(c => s.add(c)); return [...s]; } function getEquivalentsHelper(code) { if (typeof window !== "undefined" && typeof window.getEquivalentsHelper === "function" && window.getEquivalentsHelper !== getEquivalentsHelper) { return window.getEquivalentsHelper(code); } if (typeof getEquivalents === "function") return getEquivalents(code); return [code]; } function showAdvisorModalAndBlock() { const html = ``; document.body.insertAdjacentHTML("beforeend", html); document.getElementById("container-main")?.setAttribute("aria-hidden", "true"); } function showInfoModal(id, title, message) { const existing = document.getElementById(id); if (existing) existing.remove(); const html = ``; document.body.insertAdjacentHTML("beforeend", html); const root = document.getElementById(id); const btn = document.getElementById(`${id}-ok`); if (btn) btn.onclick = function () { root.remove(); }; } async function patchContactPreferredCampus(contactId, campusId) { const url = `https://www.vossie.net/_api/contacts(${contactId})`; const tries = [ { "btfh_preferredcampus@odata.bind": `/btfh_campuses(${campusId})` }, { "btfh_PreferredCampus@odata.bind": `/btfh_campuses(${campusId})` } ]; let lastErr = null; for (const d of tries) { try { await webapi.safeAjax({ type: "PATCH", url, contentType: "application/json", data: JSON.stringify(d) }); return; } catch (e) { lastErr = e; } } throw lastErr || new Error("Preferred campus update failed"); } function ensurePreferredCampusModal(campuses) { return new Promise(resolve => { const existing = document.getElementById("preferred-campus-required-modal"); if (existing) existing.remove(); const options = (campuses || []).map(c => ``).join(""); const html = ``; document.body.insertAdjacentHTML("beforeend", html); const root = document.getElementById("preferred-campus-required-modal"); const sel = document.getElementById("preferred-campus-select"); const err = document.getElementById("preferred-campus-error"); const btn = document.getElementById("preferred-campus-save"); const setErr = m => { if (err) err.textContent = m || ""; }; if (btn) btn.onclick = function () { const v = sel ? sel.value : ""; if (!v) { setErr("Please select a campus."); return; } try { btn.disabled = true; } catch (e) { } try { setErr(""); } catch (e) { } resolve({ campusId: v, campusName: sel && sel.options && sel.selectedIndex >= 0 ? sel.options[sel.selectedIndex].text : "" }); try { root.remove(); } catch (e) { } }; }); } async function ensurePreferredCampusSelected() { const current = ContactData && ContactData._btfh_preferredcampus_value; if (current) return current; if (!btfhCurrentProgram) { showInfoModal("preferred-campus-missing-program-modal", "Preferred campus required", "We couldn't determine your current program to load campuses. Please contact your Student Advisor."); throw new Error("Missing program for campus list"); } const campusData = await fetchData(`/fetchcampus/?id=${btfhCurrentProgram}`); const list = (campusData && campusData.results) ? campusData.results : []; if (!list.length) { showInfoModal("preferred-campus-empty-modal", "Preferred campus required", "No campuses are available for your program. Please contact your Student Advisor."); throw new Error("No campuses available"); } list.sort((a, b) => String(a.Name || "").localeCompare(String(b.Name || ""))); while (true) { const choice = await ensurePreferredCampusModal(list); const cid = (choice && choice.campusId) ? choice.campusId : ""; if (!cid) continue; try { await patchContactPreferredCampus(studentId, cid); if (ContactData) { ContactData._btfh_preferredcampus_value = cid; ContactData["_btfh_preferredcampus_value@OData.Community.Display.V1.FormattedValue"] = choice.campusName || ContactData["_btfh_preferredcampus_value@OData.Community.Display.V1.FormattedValue"]; } return cid; } catch (e) { console.error("Preferred campus patch failed", e); showInfoModal("preferred-campus-save-failed-modal", "Preferred campus required", "We couldn't save your preferred campus right now. Please try again."); } } } function goToQuote(opportunityId, studentId) { const oid = (opportunityId || "").toString().trim(); const sid = (studentId || "").toString().trim(); if (!oid || !sid) { showInfoModal( "quote-error-modal", "We couldn't open your quote", "We couldn't open your quote because some details are missing. Please try again from the enrolment page or contact a Student Advisor if this continues." ); return; } window.location.href = `/selfservice/quotecreate?opportunityid=${oid}&contactid=${sid}`; } function showQuoteGuardModal(info, oppId, studId, onResolve) { const existing = document.getElementById("quote-guard-modal"); if (existing) existing.remove(); const name = info.quoteName || info.quoteId || "this quote"; const quoteUrl = (oppId && studId) ? `/selfservice/quotecreate?opportunityid=${oppId}&contactid=${studId}` : "#"; const html = ``; document.body.insertAdjacentHTML("beforeend", html); const root = document.getElementById("quote-guard-modal"); document.getElementById("qg-continue").onclick = function () { root.remove(); if (onResolve) onResolve(true); }; document.getElementById("qg-open").onclick = function (e) { e.preventDefault(); goToQuote(oppId, studId); if (onResolve) onResolve(false); }; document.getElementById("qg-cancel").onclick = function () { root.remove(); if (onResolve) onResolve(false); }; } window.BLOCKED_BY_INTEGRATED_QUOTE = false; async function checkBlockingQuotes(studentId) { try { const query = [ "https://www.vossie.net/_api/quotes?", "$select=quoteid,bt_studentenrolled,statuscode", "&$expand=opportunityid($select=_parentcontactid_value)", "&$filter=(", "(statuscode ne 4) or (bt_studentenrolled eq true)", ") and (_opportunityid_value ne null)" ].join(""); const data = await webapi.safeAjax({ type: "GET", url: query, dataType: "json" }); if (data.value && data.value.length > 0) { for (const q of data.value) { const belongsToThisStudent = q.opportunityid && q.opportunityid._parentcontactid_value && q.opportunityid._parentcontactid_value.toLowerCase() === studentId.toLowerCase(); if (!belongsToThisStudent) continue; if (q.bt_studentenrolled === true) { window.BLOCKED_BY_INTEGRATED_QUOTE = true; showAdvisorModalAndBlock(); return true; } } } } catch (err) { console.error("Error checking for blocking quotes:", err); } return false; } async function guardOpportunityWithQuote(opportunityId, studentId) { try { if (!opportunityId) return true; const url = `https://www.vossie.net/_api/quotes?$select=quoteid,name,bt_studentenrolled,statuscode&$filter=_opportunityid_value eq ${opportunityId} and statecode eq 0&$orderby=createdon desc&$top=1`; const data = await webapi.safeAjax({ type: "GET", url, dataType: "json" }); const q = (data.value && data.value[0]) || null; if (!q || q.bt_studentenrolled === true) return true; const info = { quoteId: q.quoteid, quoteName: q.name }; return await new Promise(res => { showQuoteGuardModal(info, opportunityId, studentId, res); }); } catch (err) { console.error("Error checking existing quote for opportunity:", err); return true; } } $(document).ready(async function () { notificationMsg.show("Loading"); $("#quoteref").hide(); await loadStudent(); courseEquivalencyDict = await fetchCourseEquivalencyData(); equivalencyMap = buildEquivalencyMap(courseEquivalencyDict); if (!btfhCurrentProgramVersion || !btfhCurrentIntake) { console.warn("Missing program/intake."); } else { await ensurePriceLists(btfhCurrentProgramVersion, btfhCurrentIntake); } const blocked = await checkBlockingQuotes(studentId); if (blocked) { notificationMsg.hide(); return; } setStep(1); $("#btfh_intake").change(onVersionChange); $("#LoadModules").click(loadModules); $("#updateButton").click(updateOpportunity).hide(); await checkAndUpdatePriceLevelIfNeeded(); initAdvancedSelectionToggle(); notificationMsg.hide(); }); async function handlePaymentChange() { try { let selectedPaymentValue; let selectedPriceLevelId; if ($("#upfront").prop("checked")) { selectedPaymentValue = 757490001; selectedPriceLevelId = UpfrontPriceList; } else { selectedPaymentValue = 757490000; selectedPriceLevelId = MonthlyPriceList; } updateContactPriceLevel(studentId, selectedPriceLevelId, selectedPaymentValue); const maybeOppId = await checkForExistingOpportunity(studentId); if (maybeOppId) { opportunityId = maybeOppId; let patchData = { "pricelevelid@odata.bind": `/pricelevels(${selectedPriceLevelId})`, "edv_pricelistselection": selectedPaymentValue }; await webapi.safeAjax({ type: "PATCH", url: `https://www.vossie.net/_api/opportunities(${opportunityId})`, contentType: "application/json", data: JSON.stringify(patchData) }); } } catch (err) { console.error("Error in handlePaymentChange:", err); } } function SubmitToFlow(jsonPayload) { return new Promise((resolve, reject) => { var wsUrl = "https://47908eaddc494c60a0715c5a43a520.fe.environment.api.powerplatform.com:443/powerautomate/automations/direct/workflows/0a513a63862f4f73a7b069f943b89882/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=Yrok_8uP2I2nidzGNgMWhNm_wYbUxh6_hZAAEKJCog4"; var jsonRequest = jsonPayload; webapi.safeAjax({ type: 'POST', url: wsUrl, data: jsonRequest, contentType: "application/json", dataType: 'json', timeout: 120000, success: function (res) { resolve(res); }, error: function (jqXHR, textStatus, errorThrown) { let detail = errorThrown || textStatus || "Error"; try { if (jqXHR && jqXHR.responseText) { console.error("SubmitToFlow error", jqXHR.status, jqXHR.responseText); try { const parsed = JSON.parse(jqXHR.responseText); if (parsed && parsed.error && parsed.error.message) { detail = parsed.error.message; } } catch (e) { } } } catch (e) { } const msg = textStatus === "NoResponse" ? "Submission processed; create quote" : detail; reject(new Error(`${msg}`)); } }); }); } function javascript_abort() { throw new Error('The script has been aborted and will not continue'); } async function loadStudent() { notificationMsg.show("Loading data"); try { const urlParams = new URLSearchParams(window.location.search); studentId = resolveStudentId(urlParams.get("studentid") || urlParams.get("contactid") || studentId); if (!studentId) { console.warn("Missing studentId; cannot load contact."); return; } const data = await webapi.safeAjax({ url: `https://www.vossie.net/_api/contacts(${studentId})?$expand=mshied_CurrentAcademicPeriodId`, type: "GET", headers: { "Accept": "application/json", "Content-Type": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" } }); ContactData = data; btfhCurrentProgram = data["_mshied_currentprogramid_value"]; document.getElementById('currentprogramname').innerHTML = data["_mshied_currentprogramid_value@OData.Community.Display.V1.FormattedValue"]; btfhCurrentProgramVersion = data["_mshied_currentprogramversion_value"]; btfhCurrentProgramVersionText = data["_mshied_currentprogramversion_value@OData.Community.Display.V1.FormattedValue"]; if (btfhCurrentProgramVersionText && btfhCurrentProgramVersionText.toLowerCase().includes("part")) modeId = true; btfhMaxCreditValue = data["bt_creditvalue"]; document.getElementById('currentprogramversion').innerHTML = btfhCurrentProgramVersionText; currentAcademicPeriod = data["mshied_CurrentAcademicPeriodId"]; if (currentAcademicPeriod) { btfhCurrentIntake = currentAcademicPeriod["_bt_intake_value"]; document.getElementById('currentintake').innerHTML = currentAcademicPeriod["_bt_intake_value@OData.Community.Display.V1.FormattedValue"]; } else { btfhCurrentIntake = null; } const priceLevelId = data["edv_paymentpreference"], priceLevelName = data["edv_paymentpreference@OData.Community.Display.V1.FormattedValue"] || null; if (priceLevelId) { existingContactPaymentValue = priceLevelId; } else { existingContactPaymentValue = 757490001; } await ensurePreferredCampusSelected(); try { const intakeYear = extractIntakeYear($("#btfh_intake option:selected").text()); } catch { } try { programVersionsDict = await fetchProgramVersionData(); } catch (err) { console.error("Exception fetching programVersionsDict:", err); } displayElectiveInstructions(btfhCurrentProgramVersion); var cont = document.getElementById('student-information'), label = document.createElement('label'); const campusName = (data["_btfh_preferredcampus_value@OData.Community.Display.V1.FormattedValue"] || "").toString().trim(); label.textContent = `${data["firstname"]} ${data["lastname"]} Student Number: ${data["msdyn_contactpersonid"]} Campus: ${campusName || 'Not available'} Finance Block Status: ${data["btfo_financeblock"] ? 'Outstanding' : 'Good Standing'}`; cont.appendChild(label); const det = document.createElement('details'); det.id = 'submission-details-toggle'; det.style.display = 'inline-block'; det.style.marginLeft = '8px'; const sum = document.createElement('summary'); sum.textContent = '(i) Submission details'; sum.style.cursor = 'pointer'; det.appendChild(sum); const panel = document.createElement('div'); panel.id = 'submission-details-panel'; panel.style.marginTop = '8px'; panel.style.padding = '8px'; panel.style.border = '1px solid #ddd'; panel.style.borderRadius = '8px'; panel.style.background = '#f9f9f9'; det.appendChild(panel); det.addEventListener('toggle', async function () { if (!det.open) return; if (panel.dataset.loaded === '1') return; panel.textContent = 'Loading...'; try { if (typeof buildOpportunityRequirementInfoHtml === 'function') { const html = await buildOpportunityRequirementInfoHtml(); panel.innerHTML = html || 'Submission details are not available yet.'; panel.dataset.loaded = '1'; return; } } catch (e) { console.error('Submission details load failed', e); } panel.textContent = 'Submission details are not available yet. Please try again in a moment.'; }); cont.appendChild(det); cont.appendChild(document.createElement('br')); var academicblock = false, academicblocklookup = data["_mshied_studentstatusid_value"], finblock = data["btfo_financeblock"]; if (academicblocklookup === "Academic Block") { academicblock = true; showInfoModal( "academic-block-modal", "Academic block on your account", "You currently have an academic block on your account. Please contact your Student Advisor for assistance." ); javascript_abort(); } if (finblock === true) { showInfoModal( "finance-block-modal", "Finance block on your account", "You currently have an outstanding balance on your account. Please contact your Student Advisor before continuing." ); javascript_abort(); } if (!finblock && !academicblock) { setIntakeValue(btfhCurrentProgram); $.getJSON(`/fetchintake/?id=${btfhCurrentProgram}`, function (intakeData) { intakeDict = intakeData; if (intakeData.results.length > 0) { const preferred = intakeData.results.find(i => i.Id === btfhCurrentIntake); btfhIntake = intakeData; if (preferred) $("#btfh_intake").val(preferred.Id); else showInfoModal( "intake-missing-modal", "Select your intake", "Please select your intake from the list before continuing." ); } }); } } catch (e) { console.error("Contact load error", e); } } async function updateStudentProgress(studentId, programId, intakeId) { if (!qualPlannerData || !qualPlannerData.results || !qualPlannerData.results.length) { console.warn("No planner data."); return; } await ensureCourseHistoryLoaded(studentId); if (!courseHistoryData || !courseHistoryData.value || !courseHistoryData.value.length) { console.warn("No course history."); return; } const plannerForSelection = (futurePlannerData && futurePlannerData.results && futurePlannerData.results.length) ? futurePlannerData : qualPlannerData; _initPlannerMinOfferingYear(plannerForSelection); const courseProgress = await analyzeCourseProgress(courseHistoryData, plannerForSelection, qualProgData, fullCourseHistoryData); outStandingModules = await getOutstandingModules( plannerForSelection, courseProgress, courseProgress.qualProgData, courseHistoryData ); updateHtml(outStandingModules, outStandingModules.requirements); appendAppliedModuleRulesToInstructions(); } function refreshOutstandingSelection() { if (!outStandingModules || !outStandingModules.requirements) return; try { updateHtml(outStandingModules, outStandingModules.requirements); } catch (e) { console.warn("refreshOutstandingSelection failed", e); } } async function fetchData(url) { if (url.startsWith('https://www.vossie.net/_api')) { return webapi.safeAjax({ type: "GET", url, dataType: "json" }) .then(res => res).catch(err => { console.error("Error fetching data with safeAjax:", err); throw err; }); } else { const resp = await fetch(url); return await resp.json(); } } async function ensureCourseHistoryLoaded(studentId) { if (courseHistoryData && fullCourseHistoryData) return; const url = `https://www.vossie.net/_api/mshied_coursehistories?$filter=_mshied_studentid_value%20eq%20${studentId} and (bt_modulestatus ne 206440000)&$expand=mshied_CourseId,bt_Product`; const data = await fetchData(url); courseHistoryData = data; fullCourseHistoryData = data; } function gateModulesByYear( qualProgData, yearlyCreditsEarned, yearlyTotalCredits, yearlyCoreCredits, yearlyCoreCreditsEarned ) { if (window && typeof window._mrGateModulesByYear === "function") { return window._mrGateModulesByYear( qualProgData, yearlyCreditsEarned, yearlyTotalCredits, yearlyCoreCredits, yearlyCoreCreditsEarned ); } const y1Total = yearlyTotalCredits[1] || 0, y2Total = yearlyTotalCredits[2] || 0, y1Earned = yearlyCreditsEarned[1] || 0, y2Earned = yearlyCreditsEarned[2] || 0; const y1CoreTotal = yearlyCoreCredits[1] || 0, y2CoreTotal = yearlyCoreCredits[2] || 0, y1CoreEarned = yearlyCoreCreditsEarned[1] || 0, y2CoreEarned = yearlyCoreCreditsEarned[2] || 0; const year1Pct = y1Total ? y1Earned / y1Total : 0, year2Pct = y2Total ? y2Earned / y2Total : 0; return qualProgData.filter(m => { const code = m.ModuleCode || m.ProductCode || m.ProductNumber; const base = String(code || '').split('-')[0]; const yr = getModuleYear(base); if (!Number.isFinite(yr) || yr <= 0) return false; if (yr <= 1) return true; const prev = yr - 1; const prevTot = yearlyTotalCredits[prev] || 0; const prevPct = prevTot ? ((yearlyCreditsEarned[prev] || 0) / prevTot) : 0; const prevCoreDone = (yearlyCoreCreditsEarned[prev] || 0) >= (yearlyCoreCredits[prev] || 0); return !(prevPct < 0.40 && !prevCoreDone); }); } async function updateProgressionAnalysisTable( maxYear, yearlyCoreCreditsEarned, yearlyCreditsEarned, yearlyCoreCredits, yearlyTotalCredits, coreModules, totalCreditsEarned, highestYearCompleted ) { const createHeader = (headers) => { const headerRow = document.createElement('tr'); headers.forEach(header => { const th = document.createElement('th'); th.className = 'table-cell'; th.innerHTML = header; headerRow.appendChild(th); }); return headerRow; }; const calcPct = (yC, tC) => { if (tC === 0) return 0; return Math.min((yC / tC) * 100, 100).toFixed(2); }; const isProgAllowed = (pct, tC) => { if (tC <= 0) return "Credits total unknown for this year"; return pct >= 40 ? "Can progress to next level." : "Below threshold"; }; const chkYearCore = (yr, pct, cMods, qpData) => { const yearCoreCodes = qpData .filter(c => deriveYear(c) === yr && isCoreLike(c)) .map(c => c.ModuleCode); if (pct === 100) return true; return yearCoreCodes.every(code => qpData.some(c => c.ModuleCode === code && ['PASSED', 'CREDIT', 'COMPETENT', 'CREDIT EXEMPTION'].includes(c.ModuleStatus) ) ); }; const table = document.createElement('table'); table.className = 'progression-table'; const thead = document.createElement('thead'); thead.appendChild(createHeader([ 'Academic Year', 'Core Credits
Percentage Complete', 'Progression' ])); table.appendChild(thead); const tbody = document.createElement('tbody'); for (let year = 1; year <= maxYear; year++) { const yC = yearlyCreditsEarned[year] || 0; const tC = yearlyTotalCredits[year] || 0; const cC = yearlyCoreCredits[year] || 0; const cEarned = yearlyCoreCreditsEarned[year] || 0; const pct = calcPct(yC, tC); const prog = isProgAllowed(pct, tC); const coreDone = chkYearCore(year, pct, coreModules, qualProgData); if (tC > 0) { const row = document.createElement('tr'); const yearCell = document.createElement('td'); yearCell.className = 'table-cell'; yearCell.textContent = year; row.appendChild(yearCell); const creditsCell = document.createElement('td'); creditsCell.className = 'table-cell'; const summarySpan = document.createElement('span'); summarySpan.textContent = `${yC} / ${tC} credits (${pct}%) (Core - ${cC})`; const infoBtn = document.createElement('button'); infoBtn.type = 'button'; infoBtn.className = 'year-info-btn'; infoBtn.textContent = 'i'; infoBtn.style.marginLeft = '4px'; infoBtn.title = 'Show credit breakdown for this year'; const detailsDiv = document.createElement('div'); detailsDiv.className = 'year-credit-breakdown'; detailsDiv.style.display = 'none'; detailsDiv.style.marginTop = '4px'; detailsDiv.style.fontSize = '0.85em'; const earnedBreakdown = (window._yearlyCreditBreakdown && window._yearlyCreditBreakdown[year]) || []; const requiredBreakdown = (window._yearlyRequiredBreakdown && window._yearlyRequiredBreakdown[year]) || []; const getRootFromCode = (code) => { const c = (code || "").toString().toUpperCase().trim(); if (!c) return ""; const dash = c.indexOf("-"); return dash >= 0 ? c.substring(0, dash) : c; }; const requiredRoots = new Set( (requiredBreakdown || []) .map(item => item.root || getRootFromCode(item.code)) .filter(Boolean) ); const creditedRootsRaw = new Set( earnedBreakdown .map(item => item.root || getRootFromCode(item.code)) .filter(Boolean) ); const creditedRoots = new Set(creditedRootsRaw); if (typeof getEquivalents === "function") { creditedRootsRaw.forEach(r => { (getEquivalents(r) || []).forEach(eqCode => { const eqRoot = getRootFromCode(eqCode); if (eqRoot) creditedRoots.add(eqRoot); }); }); } const ruleEqMap = window._moduleRuleEquivalences; if (ruleEqMap && typeof ruleEqMap.get === "function") { creditedRootsRaw.forEach(newRoot => { const entry = ruleEqMap.get(newRoot); if (entry && entry.oldRoots && typeof entry.oldRoots.forEach === "function") { entry.oldRoots.forEach(or => { const orRoot = getRootFromCode(or); if (orRoot) creditedRoots.add(orRoot); }); } }); } const completedNotCredited = []; const plannerRoots = new Set(); if (qualPlannerData && qualPlannerData.results && qualPlannerData.results.length) { (qualPlannerData.results || []).forEach(pm => { const r = getRootFromCode(pm.ModuleCode || pm.ProductCode || pm.ProductNumber); if (r) plannerRoots.add(r); }); } (qualProgData || []).forEach(m => { const codeForRoot = m.ModuleCode || m.ProductCode || m.ProductNumber; const base = String(codeForRoot || "").split("-")[0]; const yr = getModuleYear(base); if (yr !== year) return; const st = (m.ModuleStatus || "").toUpperCase(); if (!["PASSED", "CREDIT", "COMPETENT", "CREDIT EXEMPTION"].includes(st)) return; const root = getRootFromCode(codeForRoot); if (!root || creditedRoots.has(root)) return; if (requiredRoots.size && !requiredRoots.has(root)) return; const credits = Number(m.Credits) || 0; if (!credits) return; completedNotCredited.push({ code: m.ModuleCode || m.ProductCode || "", credits, name: m.Name || m.ProductName || "" }); }); if (typeof courseHistoryData !== "undefined" && courseHistoryData && Array.isArray(courseHistoryData.value)) { courseHistoryData.value.forEach(rec => { const fullCode = (rec.mshied_CourseId && rec.mshied_CourseId.mshied_coursenumber || "").toString().toUpperCase().trim(); if (!fullCode) return; const baseHist = fullCode.split("-")[0]; const histYear = getModuleYear(baseHist); if (histYear !== year) return; const statusFormatted = (rec["bt_modulestatus@OData.Community.Display.V1.FormattedValue"] || "").toUpperCase(); if (!["PASSED", "CREDIT", "COMPETENT", "CREDIT EXEMPTION"].includes(statusFormatted)) return; const root = getRootFromCode(fullCode); if (!root || creditedRoots.has(root)) return; if (requiredRoots.size && !requiredRoots.has(root)) return; if (plannerRoots.size && !plannerRoots.has(root)) return; const credits = Number(rec.mshied_CourseId && rec.mshied_CourseId.bt_creditvalue) || 0; if (!credits) return; completedNotCredited.push({ code: baseHist, credits, name: rec.mshied_name || "" }); }); } const seenNC = new Set(); const uniqueCompletedNotCredited = completedNotCredited.filter(item => { const key = (item.code || item.name || "") + "|" + item.credits; if (seenNC.has(key)) return false; seenNC.add(key); return true; }); let requiredHtml = ""; if (requiredBreakdown.length) { requiredHtml = '
Modules composing required credits:'; } let earnedHtml = ""; if (earnedBreakdown.length) { earnedHtml = '
Modules counted as earned:'; } let notCreditedHtml = ""; if (uniqueCompletedNotCredited.length) { notCreditedHtml = '
Completed here but not counted as level credits:'; } let equivalenceHtml = ""; if (typeof getEquivalents === "function") { const eqGroups = []; const seenEqRoots = new Set(); earnedBreakdown.forEach(item => { const root = item.root || getRootFromCode(item.code); if (!root) return; const eqCodes = getEquivalents(root) || []; const roots = Array.from(new Set(eqCodes.map(c => getRootFromCode(c)).filter(Boolean))); const active = roots.filter(r => earnedBreakdown.some(e => (e.root || getRootFromCode(e.code)) === r) ); if (active.length > 1) { active.forEach(r => seenEqRoots.add(r)); eqGroups.push(active); } }); if (eqGroups.length) { equivalenceHtml = '
Equivalence groups applied:'; } } let rulesHtml = ""; const rulesLog = Array.isArray(window._appliedModuleRulesLog) ? window._appliedModuleRulesLog : []; if (rulesLog.length) { const rulesForYear = rulesLog.filter(entry => { const allCodes = []; (entry.oldModules || []).forEach(c => allCodes.push(c)); (entry.newModules || []).forEach(c => allCodes.push(c)); return allCodes.some(code => { const root = getRootFromCode(code); if (!root) return false; const yr = getModuleYear(root); return yr === year; }); }); if (rulesForYear.length) { rulesHtml = '
Module rules affecting this year:'; } } detailsDiv.innerHTML = `Required credits: ${tC}
` + `Credits earned: ${yC}
` + `Core required: ${cC}
` + `Core earned: ${cEarned}
` + `Rule: 40% credits or all core.` + requiredHtml + earnedHtml + notCreditedHtml + equivalenceHtml + rulesHtml; infoBtn.addEventListener('click', () => { const isVisible = detailsDiv.style.display === 'block'; detailsDiv.style.display = isVisible ? 'none' : 'block'; }); creditsCell.appendChild(summarySpan); creditsCell.appendChild(infoBtn); creditsCell.appendChild(detailsDiv); row.appendChild(creditsCell); const progCell = document.createElement('td'); progCell.className = 'table-cell'; progCell.textContent = coreDone ? `Year Core Modules Completed. ${prog}` : `Core modules not yet complete. ${prog}`; row.appendChild(progCell); tbody.appendChild(row); } } table.appendChild(tbody); const infoDiv = document.createElement('div'); const infoTable = document.createElement('table'); const infoThead = createHeader(['Item', 'Value']); infoTable.appendChild(infoThead); const infoTbody = document.createElement('tbody'); const infoRows = [ ['Highest year of qualification', maxYear], ['Highest year qualifying for progression', highestYearCompleted] ]; infoRows.forEach(([label, value]) => { const row = document.createElement('tr'); const labelCell = document.createElement('td'); const valueCell = document.createElement('td'); labelCell.className = 'table-cell'; valueCell.className = 'table-cell'; labelCell.textContent = label; valueCell.textContent = value; row.appendChild(labelCell); row.appendChild(valueCell); infoTbody.appendChild(row); }); infoTable.appendChild(infoTbody); infoDiv.appendChild(infoTable); const container = document.getElementById('progression-analysis'); if (!container) return; container.innerHTML = ''; container.appendChild(table); container.appendChild(infoDiv); } function initAdvancedSelectionToggle() { const totalsContainer = document.getElementById('totals-container'); const fallback = document.getElementById('progression-analysis'); const target = totalsContainer || fallback; if (!target) return; const existingWrap = document.getElementById('advanced-selection-toggle-wrap'); if (existingWrap && existingWrap.parentNode) existingWrap.parentNode.removeChild(existingWrap); const btn = document.createElement('button'); btn.type = 'button'; btn.id = 'advanced-selection-toggle'; btn.className = 'pill'; btn.style.marginLeft = '6px'; const setLabel = () => { btn.textContent = modeId ? 'Advanced ON' : 'Advanced OFF'; }; setLabel(); btn.addEventListener('click', () => { modeId = !modeId; setLabel(); refreshOutstandingSelection(); }); const label = document.createElement('span'); label.textContent = 'Selection mode:'; const wrap = document.createElement('div'); wrap.id = 'advanced-selection-toggle-wrap'; wrap.style.marginTop = '4px'; wrap.appendChild(label); wrap.appendChild(btn); target.appendChild(wrap); } function getMaxCredits(year, qualPlannerData, programVersionsDict) { if (window && typeof window._mrGetMaxCredits === "function") { return window._mrGetMaxCredits(year, qualPlannerData, programVersionsDict); } if (!qualPlannerData || !qualPlannerData.results) { return 0; } let total = 0; qualPlannerData.results.forEach(module => { if (EXCLUDE_S_CODES && isSCodeModule(module)) return; const modYear = deriveYear(module); if (modYear !== year) { return; } if (module.Repeat === "true") { return; } if (module.Type === "Optional") { return; } const credits = parseFloat(module.Credits) || 0; if (module.Type === "Core" || module.ModuleClass === "Fundamental") { total += credits; } else if (module.Elective === "true") { total += credits * 0.5; } }); total = Math.ceil(total); const found = programVersionsDict.results.find(x => (parseInt(x.Name.slice(-1)) || 1) === year); const foundValue = found ? parseInt(found.CreditValue, 10) : 0; return foundValue > 0 ? (total > 0 ? Math.min(total, foundValue) : foundValue) : total; }