async function getOutstandingModules(qualPlannerData, studentProgress, qualProgData, courseHistoryData) {
let outstandingCoreModules = [],
unregisteredOutstandingCoreModules = [],
registeredOutstandingCoreModules = [],
outstandingElectiveModules = [],
outstandingRepeaterModules = [],
preCoRecNotMetModules = [],
allNewPeriodModules = [],
allModules = [],
extModules = [];
const backlogCoreCodes = new Set();
let yearCompleted = parseInt(studentProgress.highestYearCompleted),
yearToConsider = studentProgress.isExitYear
? yearCompleted
: yearCompleted >= 0
? yearCompleted + 1
: 1;
const allowedIntakeYears = window.getAllowedIntakeYearsWindow && window.getAllowedIntakeYearsWindow();
const selectedIntakeYear = getSelectedIntakeYear();
let currentlyRegistered = new Set(
(qualProgData || [])
.filter(c => isInProgressLikeStatus(c.ModuleStatus))
.map(c => c.ModuleCode)
);
(qualProgData || []).forEach(m => {
if (isAdminModuleLocal(m)) return;
const moduleCode = m.ModuleCode;
const yr = deriveYear(m);
if (yr <= yearToConsider) {
const intakeYear = getIntakeYearForModule(m);
const isBacklogYear = !studentProgress.isExitYear && yr < yearToConsider;
if (!isBacklogYear) {
if (Number.isFinite(intakeYear) && intakeYear >= 1900) {
if (!isIntakeYearAllowedForSelection(intakeYear, allowedIntakeYears, selectedIntakeYear)) {
return;
}
}
} else {
if (
Number.isFinite(selectedIntakeYear) && selectedIntakeYear >= 1900 &&
Number.isFinite(intakeYear) && intakeYear >= 1900 &&
intakeYear > selectedIntakeYear
) {
return;
}
}
const makeKey = obj =>
`${(obj.ProductCode || obj.ModuleCode || '').toUpperCase()}|${deriveYear(obj) || 0}`;
const key = makeKey(m);
if (!allModules.some(x => makeKey(x) === key)) {
allModules.push(m);
}
if (
isCoreLike(m) &&
!studentProgress.passedCoreModules.some(x => x.ModuleCode === moduleCode) &&
!studentProgress.failedModules.some(x => x.ModuleCode === moduleCode)
) {
if (!outstandingCoreModules.some(x => x.ModuleCode === moduleCode)) {
outstandingCoreModules.push(m);
if (!allNewPeriodModules.some(x => x.ModuleCode === moduleCode)) {
allNewPeriodModules.push(m);
}
const isBacklogYear = !studentProgress.isExitYear && yr < yearToConsider;
if (isBacklogYear) backlogCoreCodes.add(moduleCode);
if (isBacklogYear && !outstandingElectiveModules.some(x => x.ModuleCode === moduleCode)) {
const em = Object.assign({}, m, { Elective: "true" });
outstandingElectiveModules.push(em);
}
if (currentlyRegistered.has(moduleCode)) {
if (!registeredOutstandingCoreModules.some(x => x.ModuleCode === moduleCode)) {
registeredOutstandingCoreModules.push(m);
}
} else {
if (!unregisteredOutstandingCoreModules.some(x => x.ModuleCode === moduleCode)) {
unregisteredOutstandingCoreModules.push(m);
}
}
}
}
}
if (
(m.Elective === "true" || m.Type === "Optional") &&
!["PASSED", "CREDIT", "COMPETENT", "CREDIT EXEMPTION"].includes((m.ModuleStatus || "").toUpperCase()) &&
!isInProgressLikeStatus(m.ModuleStatus) &&
!studentProgress.passedCoreModules.some(x => x.ModuleCode === moduleCode)
) {
if (!outstandingElectiveModules.some(x => x.ModuleCode === moduleCode)) {
outstandingElectiveModules.push(m);
if (!allNewPeriodModules.some(x => x.ModuleCode === moduleCode)) {
allNewPeriodModules.push(m);
}
}
}
if (!currentlyRegistered.has(moduleCode)) {
if (qualProgData.some(c => c.ModuleCode === moduleCode && c.ModuleStatus === "FAILED")) {
if (!outstandingRepeaterModules.some(x => x.ModuleCode === moduleCode)) {
outstandingRepeaterModules.push(m);
}
if (!allNewPeriodModules.some(x => x.ModuleCode === moduleCode)) {
allNewPeriodModules.push(m);
}
}
}
});
try {
if (courseHistoryData && Array.isArray(courseHistoryData.value)) {
const currentCodes = new Set(qualProgData.map(q => q.ModuleCode));
const creditedRoots = new Set();
if (window._yearlyCreditBreakdown) {
Object.values(window._yearlyCreditBreakdown).forEach(list => {
(list || []).forEach(item => {
if (item && item.root) {
creditedRoots.add(item.root.toString().toUpperCase());
}
});
});
}
courseHistoryData.value.forEach(rec => {
const mc = rec.mshied_CourseId && rec.mshied_CourseId.mshied_coursenumber;
const statusRaw = rec["bt_modulestatus@OData.Community.Display.V1.FormattedValue"];
if (!mc || !statusRaw) return;
if (!isInProgressLikeStatus(statusRaw)) return;
if (currentCodes.has(mc)) return;
const mcBase = getBaseModuleCode(mc);
const eqCodes = getEquivalents(mcBase);
const isUsedForProgression = eqCodes.some(code => {
const r = getBaseModuleCode(code);
return creditedRoots.has(r);
});
if (isUsedForProgression) return;
if (isAdminModuleLocal({ ModuleCode: mc, Type: "", ModuleClass: "" })) return;
if (registeredOutstandingCoreModules.some(x => x.ModuleCode === mc)) return;
const nonDegName = (rec.mshied_name || "") + " (NDP)";
const credits = (rec.mshied_CourseId && rec.mshied_CourseId.bt_creditvalue) || 0;
const productNumber = rec.bt_Product ? rec.bt_Product["_msdyn_sharedproductdetailid_value@OData.Community.Display.V1.FormattedValue"] : "";
const productCode = rec.mshied_CourseId ? rec.mshied_CourseId.mshied_code : null;
const nonDegreeMod = {
Id: "",
Name: nonDegName,
Repeat: "false",
Credits: credits,
YearOfOffering: rec.bt_year || 0,
YearOfUnderTaking: rec["bt_yearofundertaking@OData.Community.Display.V1.FormattedValue"],
StartsInBlock: productNumber ? `Block ${parseInt(productNumber.slice(-2, -1) || "0", 10)}` : "",
ModulePeriod: productNumber ? parseInt(productNumber.slice(-2, -1) || "0", 10) : 0,
ProductNumber: productNumber,
ProductId: rec.bt_Product ? rec.bt_Product.productid : null,
ProductCode: productCode,
ProductName: rec.mshied_CourseId ? rec.mshied_CourseId["_bt_product_value@OData.Community.Display.V1.FormattedValue"] : "",
Year: getModuleYear(mc.split("-")[0]),
Elective: "false",
Type: "Core",
ModuleType: "Core",
ModuleClass: "",
ModuleCode: mc,
ModuleStatus: statusRaw.toUpperCase()
};
if (!allModules.some(m => m.ModuleCode === mc)) {
allModules.push(nonDegreeMod);
}
registeredOutstandingCoreModules.push(nonDegreeMod);
});
}
} catch (e) {
console.warn("NDP build error", e);
}
(studentProgress.extModules || []).forEach(mod => {
const c = mod.ModuleCode;
if (!allModules.some(m => m.ModuleCode === c)) {
allModules.push(mod);
}
if (!allNewPeriodModules.some(m => m.ModuleCode === c)) {
allNewPeriodModules.push(mod);
}
if (!extModules.some(m => m.ModuleCode === c)) {
extModules.push(mod);
}
});
let requirementsList = { preRequisiteModules: [], coRequisiteModules: [] };
if (window.getPreAndCoRequisites) {
requirementsList = await window.getPreAndCoRequisites(allNewPeriodModules);
}
const filterByIntakeWindow = m => {
const iy = getIntakeYearForModule(m);
const isBacklog = backlogCoreCodes && backlogCoreCodes.has(m.ModuleCode);
if (isBacklog) {
if (!Number.isFinite(iy) || iy < 1900) return true;
if (!Number.isFinite(selectedIntakeYear) || selectedIntakeYear < 1900) return true;
return iy <= selectedIntakeYear;
}
if (!Number.isFinite(iy) || iy < 1900) return true;
return isIntakeYearAllowedForSelection(iy, allowedIntakeYears, selectedIntakeYear);
};
outstandingCoreModules = (outstandingCoreModules || []).filter(filterByIntakeWindow);
unregisteredOutstandingCoreModules = (unregisteredOutstandingCoreModules || []).filter(filterByIntakeWindow);
registeredOutstandingCoreModules = (registeredOutstandingCoreModules || []).filter(filterByIntakeWindow);
outstandingElectiveModules = (outstandingElectiveModules || []).filter(filterByIntakeWindow);
outstandingRepeaterModules = (outstandingRepeaterModules || []).filter(filterByIntakeWindow);
allNewPeriodModules = (allNewPeriodModules || []).filter(filterByIntakeWindow);
allModules = (allModules || []).filter(filterByIntakeWindow);
extModules = (extModules || []).filter(filterByIntakeWindow);
let plannerForOutstanding;
try { plannerForOutstanding = buildPlannerForCurrentYear(); }
catch (e) {
plannerForOutstanding =
futurePlannerData && futurePlannerData.results && futurePlannerData.results.length
? futurePlannerData
: qualPlannerData;
}
finalizePlanApproachB(
allModules,
preCoRecNotMetModules,
outstandingElectiveModules,
outstandingRepeaterModules,
requirementsList,
plannerForOutstanding,
studentProgress,
allNewPeriodModules,
outstandingElectiveModules,
outstandingRepeaterModules
);
const blockedSet = new Set(preCoRecNotMetModules.map(m => (m.ModuleCode || '').toUpperCase()));
if (EXCLUDE_S_CODES) {
const keep = m => {
if (!isSCodeModule(m)) return true;
const b = String(m.ModuleCode || "").split("-")[0].toUpperCase();
return !!(studentProgress.passedModules && studentProgress.passedModules.has(b));
};
outstandingCoreModules = (outstandingCoreModules || []).filter(keep);
unregisteredOutstandingCoreModules = (unregisteredOutstandingCoreModules || []).filter(keep);
registeredOutstandingCoreModules = (registeredOutstandingCoreModules || []).filter(keep);
outstandingElectiveModules = (outstandingElectiveModules || []).filter(keep);
outstandingRepeaterModules = (outstandingRepeaterModules || []).filter(keep);
allNewPeriodModules = (allNewPeriodModules || []).filter(keep);
}
try {
const legacyRoots = new Set();
const addRoot = v => {
if (!v) return;
const r = String(v).toUpperCase().trim();
if (r) legacyRoots.add(r);
};
const eqMap = window._moduleRuleEquivalences;
if (eqMap) {
if (typeof eqMap.forEach === "function") {
eqMap.forEach(entry => {
if (!entry || entry.mode !== "all" || !entry.oldRoots) return;
if (typeof entry.oldRoots.forEach === "function") {
entry.oldRoots.forEach(addRoot);
} else if (Array.isArray(entry.oldRoots)) {
entry.oldRoots.forEach(addRoot);
}
});
} else if (typeof eqMap === "object") {
Object.keys(eqMap).forEach(k => {
const entry = eqMap[k];
if (!entry || entry.mode !== "all" || !entry.oldRoots) return;
if (typeof entry.oldRoots.forEach === "function") {
entry.oldRoots.forEach(addRoot);
} else if (Array.isArray(entry.oldRoots)) {
entry.oldRoots.forEach(addRoot);
}
});
}
}
const log = window._appliedModuleRulesLog;
if (Array.isArray(log)) {
log.forEach(r => {
if (!r || !r.oldModules || !r.oldModules.length) return;
if (r.type === 948781000 || r.type === 948781006) {
r.oldModules.forEach(addRoot);
}
});
}
if (legacyRoots.size) {
const isLegacy = m => {
if (!m) return false;
const raw = (m.ModuleCode || m.ProductCode || m.ProductNumber || "");
if (!raw) return false;
const base = String(raw).split("-")[0].toUpperCase().trim();
if (!base) return false;
return legacyRoots.has(base);
};
const flt = list => (list || []).filter(x => !isLegacy(x));
outstandingCoreModules = flt(outstandingCoreModules);
unregisteredOutstandingCoreModules = flt(unregisteredOutstandingCoreModules);
registeredOutstandingCoreModules = flt(registeredOutstandingCoreModules);
outstandingElectiveModules = flt(outstandingElectiveModules);
outstandingRepeaterModules = flt(outstandingRepeaterModules);
allNewPeriodModules = flt(allNewPeriodModules);
allModules = flt(allModules);
}
} catch (e) {
console.warn("Legacy rule filter failed", e);
}
return {
core: outstandingCoreModules,
unregisteredCore: unregisteredOutstandingCoreModules,
registeredCore: registeredOutstandingCoreModules,
requirements: requirementsList,
corecs: preCoRecNotMetModules,
elective: outstandingElectiveModules,
repeater: outstandingRepeaterModules,
current: allNewPeriodModules,
extmodules: extModules,
all: allModules,
blockedSet,
backlogCoreCodes
};
}
function getModuleDescription(module, yearCode, requirements) {
if (window && typeof window._mrRenderModuleDescription === "function") {
return window._mrRenderModuleDescription(
module,
yearCode,
requirements,
(typeof qualPlannerData !== "undefined" ? qualPlannerData : null)
);
}
const co = requirements?.coRequisiteModules || [];
const pr = requirements?.preRequisiteModules || [];
const cos = co.filter(r => r.ModuleCode === module.ModuleCode);
const prs = pr.filter(r => r.ModuleCode === module.ModuleCode);
const coList = cos.length > 0 ? cos.map(r => r.edv_RequiredCourse.mshied_coursenumber).join(", ") : "None";
const preList = prs.length > 0 ? prs.map(r => r.edv_RequiredCourse.mshied_coursenumber).join(", ") : "None";
const tAbbrev = module.Type === "Core" ? "C" : "E";
const cAbbrev = module.ModuleClass?.[0]?.toUpperCase() || "U";
const yo = Number(module.YearOfOffering);
const yu = Number(module.YearOfUnderTaking);
const selY = (window.getSelectedIntakeYear && window.getSelectedIntakeYear()) || new Date().getFullYear();
let dispYear = selY;
if (Number.isFinite(yo) && yo >= selY) dispYear = yo;
else if (Number.isFinite(yu) && yu < selY) dispYear = yu;
else if (Number.isFinite(yo)) dispYear = yo;
else if (Number.isFinite(yu)) dispYear = yu;
let yAbbrev = String(dispYear % 100);
if (yAbbrev.length < 2) yAbbrev = "0" + yAbbrev;
const sAbbrev = module.ModuleStatus?.[0]?.toUpperCase() || "U";
const codeSpan = module.ModuleCode ? `${module.ModuleCode}` : "";
const compact = `${tAbbrev}${cAbbrev}-${yAbbrev}-${sAbbrev}`;
const baseRoot = (typeof getBaseModuleCode === "function")
? getBaseModuleCode(module.ProductCode || module.ModuleCode || "")
: String(module.ProductCode || module.ModuleCode || "").split("-")[0];
let displaySource = module;
if (baseRoot && typeof qualPlannerData !== "undefined" && qualPlannerData && Array.isArray(qualPlannerData.results)) {
const plannerMatch = qualPlannerData.results.find(p => {
const pRoot = (typeof getBaseModuleCode === "function")
? getBaseModuleCode(p.ProductCode || p.ModuleCode || "")
: String(p.ProductCode || p.ModuleCode || "").split("-")[0];
return pRoot === baseRoot;
});
if (plannerMatch) {
displaySource = {
...plannerMatch,
// Preserve module-specific status/credits where relevant
ModuleStatus: module.ModuleStatus || plannerMatch.ModuleStatus,
Credits: typeof module.Credits !== "undefined" ? module.Credits : plannerMatch.Credits,
ModuleCode: plannerMatch.ModuleCode || baseRoot,
ProductCode: plannerMatch.ProductCode || module.ProductCode,
ProductNumber: plannerMatch.ProductNumber || module.ProductNumber,
YearOfOffering: plannerMatch.YearOfOffering || module.YearOfOffering,
YearOfUnderTaking: plannerMatch.YearOfUnderTaking || module.YearOfUnderTaking
};
}
}
const displayName = displaySource.Name || displaySource.ProductName || module.Name || "";
const nameSpan = displayName
? `${displayName}`
: `Unnamed Module`;
const credSpan = module.Credits
? `Credits: ${module.Credits}`
: "";
const dpc = (window.getDisplayProductCode && window.getDisplayProductCode(displaySource)) || module.ProductCode;
const pCodeSpan = dpc ? `${dpc}` : "";
const coReqSpan = coList !== "None" ? `Co-requisites: ${coList}` : "";
const preReqSpan = preList !== "None" ? `Pre-requisites: ${preList}` : "";
const parts = [codeSpan, compact, nameSpan, credSpan, pCodeSpan, coReqSpan, preReqSpan]
.filter(Boolean)
.join(' • ');
return `
${parts}
`;
}
function findFirstNumber(module) {
let block = null, codeBlock = null;
if (module.StartsInBlock) {
const r = module.StartsInBlock.match(new RegExp("\\d+")); block = r ? Number(r[0]) : null;
}
if (module.ProductCode) {
const pc = module.ProductCode.toLowerCase(), mt = pc.match(new RegExp("-([a-z])(\\d)(\\d)$", "i"));
if (mt) codeBlock = Number(mt[2]);
}
return block !== codeBlock ? codeBlock : block;
}
async function updateHtml(outstandingModules, requirements) {
const blockedSet = outstandingModules.blockedSet ||
new Set((outstandingModules.corecs || []).map(m => (m.ModuleCode || '').toUpperCase()));
function captureSelectionState() {
const map = {};
for (let i = 0; i <= 4; i++) {
const c = document.getElementById(`block${i}-analysis`);
if (!c) continue;
const checkboxes = c.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
map[cb.value] = cb.checked;
});
}
return map;
}
const prevSelection = captureSelectionState();
function shouldShowCheckbox(m) {
const isElectiveOrOptional = (m.Elective === 'true' || m.Type === 'Optional');
return isElectiveOrOptional || modeId;
}
function updateModuleContainer(cid, mods) {
const c = document.getElementById(cid); c.innerHTML = '';
if (!mods) return;
mods.forEach(mod => {
const el = document.createElement('div'); el.className = 'module-element inline';
el.innerHTML = getModuleDescription(mod, mod.ProductCode, requirements);
if (blockedSet && blockedSet.has((mod.ModuleCode || '').toUpperCase())) {
const badge = document.createElement('span');
badge.className = 'pill warn';
badge.textContent = 'Prereqs not met';
el.appendChild(badge);
}
c.appendChild(el); c.appendChild(document.createElement('br'));
});
}
updateModuleContainer('coreunregistered-container', outstandingModules.unregisteredCore);
updateModuleContainer('coreregistered-container', outstandingModules.registeredCore);
updateModuleContainer('repeater-container', outstandingModules.repeater);
updateModuleContainer('coreq-container', outstandingModules.corecs);
const electC = document.getElementById('elective-container');
electC.innerHTML = '';
outstandingModules.elective.forEach(mod => {
const el = document.createElement('div'); el.className = 'module-element inline';
const label = document.createElement('label');
label.innerHTML = getModuleDescription(mod, mod.YearOfUnderTaking, requirements);
el.appendChild(label); electC.appendChild(el);
});
const allC = document.getElementById('all-container');
allC.innerHTML = '';
const allForDisplay = [...(outstandingModules.all || [])]
.sort((a, b) => (deriveYear(a) - deriveYear(b)) || String(a.ProductCode || a.ModuleCode || '')
.localeCompare(String(b.ProductCode || b.ModuleCode || '')));
const hasInternalCode = m => {
const code = String(m.ProductCode || m.ModuleCode || "");
return /^[A-Za-z0-9]+-[A-Za-z]\d{2}$/.test(code);
};
const bestByKey = new Map();
allForDisplay.forEach(mod => {
const base = String(mod.ProductCode || mod.ModuleCode || '').split('-')[0];
const yr = deriveYear(mod) || 0;
const rep = (String(mod.Repeat).toLowerCase() === 'true') ? 'R' : 'N';
const key = `${base}|Y${yr}|${rep}`;
if (!base) return;
const existing = bestByKey.get(key);
if (!existing || (hasInternalCode(mod) && !hasInternalCode(existing))) {
bestByKey.set(key, mod);
}
});
const finalList = Array.from(bestByKey.values())
.sort((a, b) => (deriveYear(a) - deriveYear(b)) || String(a.ProductCode || a.ModuleCode || '')
.localeCompare(String(b.ProductCode || b.ModuleCode || '')));
finalList.forEach(mod => {
const base = String(mod.ProductCode || mod.ModuleCode || '').split('-')[0];
const el = document.createElement('div');
el.className = 'module-element inline';
el.innerHTML = getModuleDescription(mod, base, requirements);
allC.appendChild(el);
allC.appendChild(document.createElement('br'));
const pcLower = String(mod.ProductCode || '').toLowerCase();
if (pcLower.endsWith('ext')) {
mod.StartsInBlock = 'Block 0';
mod.ModulePeriod = 0;
const extEl = document.createElement('div');
extEl.className = 'module-element inline';
extEl.innerHTML = getModuleDescription(mod, base, requirements);
allC.appendChild(extEl);
}
});
let totalSel = 0, totalCredits = 0;
const tContainer = document.getElementById('totals-container');
if (!tContainer) return;
let tM = document.getElementById('total-modules');
let tC = document.getElementById('total-credits');
if (!tM) {
tM = document.createElement('div');
tM.id = 'total-modules';
tContainer.appendChild(tM);
}
if (!tC) {
tC = document.createElement('div');
tC.id = 'total-credits';
tContainer.appendChild(tC);
}
function updateTotalsDisplay() {
tM.textContent = `Total Elective/Optional Credits: ${totalSel}`;
tC.textContent = `Total Selected Credits: ${totalCredits}`;
let warnEl = document.getElementById('credits-warning-pill');
if (warnEl) warnEl.remove();
if (totalCredits > 120) {
warnEl = document.createElement('span');
warnEl.id = 'credits-warning-pill';
warnEl.className = 'pill warn';
warnEl.textContent = 'Warning: recommended maximum 120 credits per year';
tContainer.appendChild(warnEl);
}
let overloadEl = document.getElementById('credits-overload-pill');
if (overloadEl) overloadEl.remove();
if (totalCredits > 180) {
overloadEl = document.createElement('span');
overloadEl.id = 'credits-overload-pill';
overloadEl.className = 'pill warn';
overloadEl.textContent = 'Warning: more than 180 credits per year requires academic approval';
tContainer.appendChild(overloadEl);
if (typeof showInfoModal === 'function' && !document.body.dataset.creditsOverloadNotified) {
showInfoModal(
'credits-180-modal',
'More than 180 credits selected',
'It is not recommended to enrol for more than 180 credits per annum as this is an excessive study load. Specific academic permission will be required. Please contact your Student Support Advisor via Connect with Student Advisor.'
);
document.body.dataset.creditsOverloadNotified = '1';
}
}
}
const Blocks = [], BCounts = [], BCountDisp = [], BMods = [];
for (let i = 0; i <= 4; i++) {
Blocks[i] = document.getElementById(`block${i}-analysis`); BCounts[i] = 0;
BMods[i] = new Set();
const bH = document.createElement('div');
bH.innerHTML = `Block ${i} - Count of Modules: `;
const cDisp = document.createElement('span');
cDisp.id = `block${i}-count`; cDisp.textContent = '0'; bH.appendChild(cDisp);
Blocks[i].innerHTML = '';
Blocks[i].appendChild(bH);
BCountDisp[i] = cDisp;
}
const backlogCoreCodes = outstandingModules.backlogCoreCodes || null;
let ruleGroups = null;
const getRootForRuleGroups = (code) => {
const c = (code || "").toString().toUpperCase().trim();
if (!c) return "";
const dash = c.indexOf("-");
return dash >= 0 ? c.substring(0, dash) : c;
};
try {
const eqMap = window._moduleRuleEquivalences;
if (eqMap) {
ruleGroups = new Map();
let gi = 0;
const handleEntry = (newRoot, entry) => {
if (!entry || entry.mode !== "any") return;
const codes = [newRoot].concat(
Array.isArray(entry.oldRoots)
? entry.oldRoots
: (entry.oldRoots && typeof entry.oldRoots.forEach === "function"
? Array.from(entry.oldRoots)
: [])
);
if (!codes.length) return;
const gid = `RG${gi++}`;
codes.forEach(cd => {
const r = getRootForRuleGroups(cd);
if (!r || ruleGroups.has(r)) return;
ruleGroups.set(r, gid);
});
};
if (typeof eqMap.forEach === "function") {
eqMap.forEach((entry, newRoot) => handleEntry(newRoot, entry));
} else if (typeof eqMap === "object") {
Object.keys(eqMap).forEach(newRoot => handleEntry(newRoot, eqMap[newRoot]));
}
if (!ruleGroups.size) ruleGroups = null;
}
} catch (e) {
console.warn("ruleGroups build failed", e);
}
function appendModule(m, bN, t) {
if (bN >= 0 && bN <= 4 && Blocks[bN]) {
const mid = m.ProductNumber || m.ModuleCode;
const blockSet = BMods[bN];
if (blockSet.has(mid)) { console.log(`Module ${mid} already in Block ${bN}, skipping.`); return; }
blockSet.add(mid);
if (shouldShowCheckbox(m) || t === "elective" || t === "elective group" || t === "optional") {
const c = document.createElement('div'); c.className = 'module-element inline'; c.dataset.credits = m.Credits || 0;
const desc = getModuleDescription(m, m.YearOfUnderTaking, requirements);
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.id = `module-${m.ProductCode}`; cb.name = `module-${m.ProductCode}`; cb.value = m.ProductCode; cb.dataset.credits = m.Credits || 0;
const isElectiveLike =
t === "elective" || t === "elective group" || t === "optional" ||
m.Elective === 'true' || m.Type === 'Optional';
if (isElectiveLike) {
cb.classList.add('elective-cb');
if (ruleGroups) {
const root = getRootForRuleGroups(m.ModuleCode || m.ProductCode || m.ProductNumber);
const gid = root && ruleGroups.get(root);
if (gid) cb.dataset.groupId = gid;
}
}
const l = document.createElement('label');
const temp = document.createElement('div');
temp.innerHTML = desc; Array.from(temp.childNodes).forEach(n => l.appendChild(n));
c.appendChild(cb); c.appendChild(l); Blocks[bN].appendChild(c);
if (prevSelection && Object.prototype.hasOwnProperty.call(prevSelection, cb.value)) {
cb.checked = !!prevSelection[cb.value];
}
cb.addEventListener('change', function () {
const pn = this.value;
const sameModule = document.querySelectorAll(`input[type="checkbox"][value="${pn}"]`);
if (this.checked) {
sameModule.forEach(oth => { if (oth !== this) oth.checked = false; });
const gid = this.dataset.groupId;
if (gid) {
const groupEls = document.querySelectorAll(`input.elective-cb[data-group-id="${gid}"]`);
groupEls.forEach(oth => { if (oth !== this) oth.checked = false; });
}
}
for (let i = 0; i <= 4; i++) updateBlockCount(i);
updateTotals();
updateElectiveStepState();
});
} else {
const el = document.createElement('div');
el.className = 'module-element inline module-no-checkbox';
el.innerHTML = getModuleDescription(m, m.YearOfUnderTaking, requirements);
el.dataset.credits = m.Credits || 0; Blocks[bN].appendChild(el);
totalSel++; totalCredits += Number(el.dataset.credits);
}
}
updateElectiveStepState();
}
function updateBlockCount(bN) {
const c = document.getElementById(`block${bN}-analysis`); if (!c) return;
let sel = 0;
const modEls = c.querySelectorAll('.module-element');
modEls.forEach(m => {
const cb = m.querySelector('input[type="checkbox"]');
if (cb) {
if (cb.checked) sel++;
} else {
sel++;
}
});
const disp = document.getElementById(`block${bN}-count`);
if (disp) disp.textContent = sel;
}
function updateTotals() {
let s = 0, sc = 0;
for (let i = 0; i <= 4; i++) {
const c = document.getElementById(`block${i}-analysis`); if (!c) continue;
const modEls = c.querySelectorAll('.module-element');
modEls.forEach(m => {
const cb = m.querySelector('input[type="checkbox"]'); if (cb && !cb.checked) return;
s++; sc += parseFloat(m.dataset.credits) || 0;
});
}
totalSel = s;
totalCredits = sc;
updateTotalsDisplay();
}
const done = new Set();
outstandingModules.unregisteredCore.forEach(m => {
const mid = m.ProductNumber || m.ModuleCode;
if (!done.has(mid)) {
const bn = findFirstNumber(m) || 0;
const isBacklog = backlogCoreCodes && backlogCoreCodes.has(m.ModuleCode);
const t = isBacklog ? 'elective' : 'core';
appendModule(m, bn, t); done.add(mid);
}
});
outstandingModules.elective.forEach(m => {
const mid = m.ProductNumber || m.ModuleCode;
if (backlogCoreCodes && backlogCoreCodes.has(m.ModuleCode)) return;
if (!done.has(mid)) {
const bn = findFirstNumber(m) || 0; appendModule(m, bn, 'elective'); done.add(mid);
}
});
outstandingModules.repeater.forEach(m => {
const mid = m.ProductNumber || m.ModuleCode;
if (!done.has(mid)) {
const bn = findFirstNumber(m) || 0; appendModule(m, bn, 'repeater'); done.add(mid);
}
});
for (let i = 0; i <= 4; i++) updateBlockCount(i);
updateTotals();
}
async function loadModules() {
if (window.BLOCKED_BY_INTEGRATED_QUOTE) return;
setStep(2);
let program = btfhCurrentProgram, intake = getSelectedIntakeIdLocal();
document.getElementById('all-container').innerHTML = "Loading...";
document.getElementById('coreregistered-container').innerHTML = "Loading...";
document.getElementById('coreunregistered-container').innerHTML = "Loading...";
document.getElementById('elective-container').innerHTML = "Loading...";
document.getElementById('repeater-container').innerHTML = "Loading...";
document.getElementById('coreq-container').innerHTML = "Loading...";
for (let i = 0; i <= 4; i++) {
document.getElementById(`block${i}-analysis`).innerHTML = `Block ${i}
Loading...`;
}
var fullqualPlanner = [];
await ensurePriceLists(btfhTargetProgamVersion || btfhCurrentProgramVersion, intake || btfhCurrentIntake);
try {
if (!campusTable || !campusTable.length) {
await populateCampusTable();
}
} catch (e) {
console.warn("Campus mapping load failed", e);
}
await updateStudentProgress(studentId, program, intake);
await checkAndUpdatePriceLevelIfNeeded();
$("#updateButton").show();
notificationMsg.hide();
}
let _lastPriceListKey = "";
let _lastQualPlannerKey = "";
let _lastFuturePlannerKey = "";
function getSelectedIntakeIdLocal() {
try {
if (typeof $ !== "function") return null;
const v = $("#btfh_intake").val();
if (Array.isArray(v)) return v.length ? v[0] : null;
if (v) return v;
const v2 = $("#btfh_intake option:selected").val();
if (Array.isArray(v2)) return v2.length ? v2[0] : null;
return v2 || null;
} catch (e) { }
return null;
}
async function ensurePriceLists(programVersionId, intakeId) {
const selIntake = intakeId || getSelectedIntakeIdLocal() || btfhCurrentIntake;
const curIntake = btfhCurrentIntake || "";
const key = `${programVersionId || ""}|${selIntake || ""}`;
if (key && key !== _lastPriceListKey) {
MonthlyPriceList = "";
UpfrontPriceList = "";
}
if (!MonthlyPriceList || !UpfrontPriceList || (key && key !== _lastPriceListKey)) {
const data = await fetchData(`/fetchplannermodules/?id=${programVersionId}&it=${selIntake}`);
if (data && data.results && data.results.length > 0) {
MonthlyPriceList = data.results[0].MonthlyPriceList;
UpfrontPriceList = data.results[0].UpfrontPriceList;
} else {
console.warn("No price list results", programVersionId, selIntake);
}
_lastPriceListKey = key;
}
// Historic planner based on the student's current intake (intakeId parameter)
const qualKey = `${btfhCurrentProgram || ""}|${curIntake}`;
if (!qualPlannerData || !qualPlannerData.results || !qualPlannerData.results.length || (qualKey && qualKey !== _lastQualPlannerKey)) {
const planner = await fetchData(`/fetchfullqualplanner/?id=${btfhCurrentProgram}&it=${curIntake || selIntake}`);
if (planner && planner.results && planner.results.length > 0) {
qualPlannerData = planner;
} else {
console.warn("No planner results", btfhCurrentProgram, curIntake || selIntake);
}
_lastQualPlannerKey = qualKey;
}
// Future planner based on the newly selected intake (if different)
try {
const futKey = `${btfhCurrentProgram || ""}|${selIntake || ""}`;
if (selIntake && selIntake !== curIntake) {
if (!futurePlannerData || !futurePlannerData.results || !futurePlannerData.results.length || (futKey && futKey !== _lastFuturePlannerKey)) {
const fut = await fetchData(`/fetchfullqualplanner/?id=${btfhCurrentProgram}&it=${selIntake}`);
if (fut && fut.results && fut.results.length > 0) {
futurePlannerData = fut;
} else {
futurePlannerData = null;
console.warn("No future planner results", btfhCurrentProgram, selIntake);
}
_lastFuturePlannerKey = futKey;
}
} else {
futurePlannerData = null;
_lastFuturePlannerKey = "";
}
} catch (e) {
console.warn("Future planner load failed", e);
}
}
var coreIds = new Set(), repeaterIds = new Set(), electiveIds = new Set(), campusTable = [], ShippingWarehouse = "", ShippingSite = "", CustomerGroup = "";
function normalizeGuid(v) {
return String(v || "").replace(/[{}]/g, "").trim().toLowerCase();
}
function isGuid(v) {
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(String(v || "").replace(/[{}]/g, "").trim());
}
function findCampusRowById(campusId) {
const cid = normalizeGuid(campusId);
if (!cid) return null;
return (campusTable || []).find(x => {
const rowId = normalizeGuid(
(x && (x.btfh_campusid || x.btfh_campusId || x.CampusId || x.campusid || x.id || x.Id))
);
return !!rowId && rowId === cid;
}) || null;
}
function showCampusShippingMissingModal(campusId, shipWarehouseId, shipSiteId) {
const campusName = (ContactData && ContactData["_btfh_preferredcampus_value@OData.Community.Display.V1.FormattedValue"]) || "";
const title = "We couldn't determine your shipping details";
const msg =
"We couldn't determine the Shipping Site and Shipping Warehouse for your campus, so we can't submit your enrolment right now." +
"
Campus: " + (campusName || normalizeGuid(campusId) || "(not set)") +
"
Shipping Warehouse: " + (shipWarehouseId || "(not set)") +
"
Shipping Site: " + (shipSiteId || "(not set)") +
"
Please try again. If this continues, contact your Student Advisor via " +
'Connect with Student Advisor.';
try {
if (typeof showInfoModal === "function") {
showInfoModal("campus-shipping-missing-modal", title, msg);
return;
}
} catch (e) { }
alert("We couldn't determine your shipping details. Please try again or contact a Student Advisor.");
}
async function resolveCampusShippingIds(campusId) {
const cid = normalizeGuid(campusId);
if (!isGuid(cid)) {
return { shippingWarehouseId: null, shippingSiteId: null, source: "invalid-campus" };
}
try {
if ((!campusTable || !campusTable.length) && typeof populateCampusTable === "function") {
await populateCampusTable();
}
} catch (e) {
console.warn("populateCampusTable failed", e);
}
const row = findCampusRowById(cid);
const fromTableWarehouse = row ? normalizeGuid(row._btfh_shippingwarehouse_value) : "";
const fromTableSite = row ? normalizeGuid(row._btfh_shippingsite_value) : "";
if (isGuid(fromTableWarehouse) && isGuid(fromTableSite)) {
return { shippingWarehouseId: fromTableWarehouse, shippingSiteId: fromTableSite, source: "campusTable" };
}
try {
const url = `https://www.vossie.net/_api/btfh_campuses(${cid})?$select=_btfh_shippingwarehouse_value,_btfh_shippingsite_value`;
const data = (typeof fetchData === "function")
? await fetchData(url)
: await webapi.safeAjax({
type: "GET",
url,
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0"
}
});
const wh = normalizeGuid(data && data._btfh_shippingwarehouse_value);
const st = normalizeGuid(data && data._btfh_shippingsite_value);
return { shippingWarehouseId: wh || null, shippingSiteId: st || null, source: "campusWebApi" };
} catch (e) {
console.error("Campus WebAPI shipping lookup failed", e);
return {
shippingWarehouseId: fromTableWarehouse || null,
shippingSiteId: fromTableSite || null,
source: "error"
};
}
}
async function populateCampusTable() {
try {
if (!btfhCurrentProgram) {
console.warn("No current program for campus.");
return;
}
const campusData = await fetchData(`/fetchcampus/?id=${btfhCurrentProgram}`);
if (campusData && campusData.results && campusData.results.length > 0) {
campusTable = campusData.results;
} else {
campusTable = [];
console.warn("No campuses for program.");
}
} catch (e) {
console.log("Error populating Campus Table:", e);
}
}
async function updateContactPriceLevel(contactId, pricelevelid, selectedPaymentValue) {
try {
const norm = v => (v || "").toString().replace(/[{}]/g, "").trim().toLowerCase();
const curPay = (typeof existingContactPaymentValue === "number")
? existingContactPaymentValue
: (ContactData && typeof ContactData.edv_paymentpreference === "number" ? ContactData.edv_paymentpreference : null);
const curPl = ContactData
? (ContactData._defaultpricelevelid_value || ContactData.defaultpricelevelid || null)
: null;
if (curPay === selectedPaymentValue && norm(curPl) === norm(pricelevelid)) {
return;
}
} catch (e) { }
var d = {
"defaultpricelevelid@odata.bind": `/pricelevels(${pricelevelid})`,
"edv_paymentpreference": selectedPaymentValue
};
webapi.safeAjax({
type: "PATCH", timeout: 30000,
url: `https://www.vossie.net/_api/contacts(${contactId})`,
contentType: "application/json", data: JSON.stringify(d),
success: function () {
console.log("Contact updated");
try { existingContactPaymentValue = selectedPaymentValue; } catch (e) { }
try {
if (ContactData) {
ContactData.edv_paymentpreference = selectedPaymentValue;
ContactData._defaultpricelevelid_value = pricelevelid;
}
} catch (e) { }
},
error: function (a, b, c) { console.log("Contact update error", c); }
});
}
async function deleteExistingQuotes(opportunityId) {
notificationMsg.show("Checking for quote changes.");
var foundQuotes = false;
var totalQuotes = 0;
var activeQuotes = 0;
var deletedQuotes = 0;
try {
const ex = await webapi.safeAjax({
type: "GET", timeout: 90000,
url: `https://www.vossie.net/_api/quotes?$filter=_opportunityid_value eq ${opportunityId}`,
dataType: "json"
});
totalQuotes = (ex && ex.value && ex.value.length) ? ex.value.length : 0;
if (totalQuotes > 0) {
for (const item of ex.value) {
if (item.statecode === 0) {
foundQuotes = true;
activeQuotes++;
try {
await webapi.safeAjax({
type: "DELETE",
url: `https://www.vossie.net/_api/quotes(${item.quoteid})`,
timeout: 90000,
dataType: "json"
});
deletedQuotes++;
console.log(`Quote ${item.quoteid} deleted.`);
notificationMsg.show("Previous quotes removed...");
} catch (e) { console.error(`Error deleting quote ${item.quoteid}:`, e); }
} else foundQuotes = true;
}
} else {
console.log('No quotes found for the opportunity.');
notificationMsg.show("Proceed to the Next Step: Create Quote.");
}
} catch (e) { console.error("Problem getting quotes for enrolment:", e); throw e; }
notificationMsg.hide();
return { foundQuotes, totalQuotes, activeQuotes, deletedQuotes };
}
async function checkForExistingOpportunity(contactId) {
try {
const ex = await webapi.safeAjax({
type: "GET",
timeout: 90000,
url: `https://www.vossie.net/_api/opportunities?$select=opportunityid,edv_pricelistselection&$filter=_parentcontactid_value eq ${contactId} and statecode eq 0`,
dataType: "json"
});
if (ex.value.length > 0) {
const opp = ex.value[0];
opportunityId = opp.opportunityid;
for (const item of ex.value) {
if (item.opportunityid !== opportunityId) {
await deleteExistingOpportunity(item.opportunityid);
}
}
if (typeof opp.edv_pricelistselection === "number") {
existingOppPaymentValue = opp.edv_pricelistselection;
}
return opportunityId;
} else {
return false;
}
} catch (e) {
console.error("Error checking for existing quote:", e);
throw e;
}
}
async function deleteExistingOpportunity(opportunityId) {
}
async function buildOpportunityRequirementInfoHtml() {
const eH = s => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
try {
if (typeof populateCampusTable === "function") {
try {
if (!campusTable || !campusTable.length) await populateCampusTable();
} catch (e) { }
}
const data = (typeof ContactData !== "undefined" && ContactData) ? ContactData : {};
const selectedProgram = btfhCurrentProgram;
const selectedProgramVersion = btfhTargetProgamVersion;
const selectedGradeLevel = data._mshied_currentprogramlevelid_value;
const selectedCampus = data._btfh_preferredcampus_value;
const selectedIntake = $("#btfh_intake option:selected").val();
const selectedIntakeText = $("#btfh_intake option:selected").text();
const selectedIntakeYear = selectedIntake ? (extractIntakeYear(selectedIntakeText) || "") : "";
const foundIntakeYear = (intakeDict && intakeDict.results ? intakeDict.results : []).find(x => x.Year === selectedIntakeYear);
const btfhTargetIntakeDate = foundIntakeYear ? foundIntakeYear.Date : null;
const selectedPaymentValue = MONTHLY_OPTION_VALUE;
const selectedPriceLevelId = MonthlyPriceList;
const campusValue = data._btfh_preferredcampus_value;
const ship = await resolveCampusShippingIds(campusValue);
const shipWarehouseId = ship && ship.shippingWarehouseId;
const shipSiteId = ship && ship.shippingSiteId;
const dataObject = {
"parentcontactid@odata.bind": `/contacts(${studentId})`,
"btfh_saletype": 948780002, "statecode": 0, "statuscode": 948780003,
"btfh_newstatusreasons": 948780010, "btfh_applicationstatus": 948780002,
"edv_pricelistselection": selectedPaymentValue,
"btfh_enrollmenttype": 948780004, "btfh_studentnumber": data.btfh_studentnumber,
"btfh_firstname": data.firstname, "btfh_lastname": data.lastname,
"btfh_studenttype": 948780001, "btfh_enrolmentstatus": false,
"btfh_academicvettedstatus": 948780005, "btfh_academicallyvetted": false,
"btfh_RegistrationCampus@odata.bind": selectedCampus ? `/btfh_campuses(${selectedCampus})` : "",
"btfh_PreferredCampus@odata.bind": selectedCampus ? `/btfh_campuses(${selectedCampus})` : "",
"btfh_SecondPreferredCampus@odata.bind": selectedCampus ? `/btfh_campuses(${selectedCampus})` : "",
"btjg_intakeyear": selectedIntakeYear, "btfh_intakedate": btfhTargetIntakeDate,
"bt_ProgramVersion@odata.bind": selectedProgramVersion ? `/mshied_programversions(${selectedProgramVersion})` : "",
"pricelevelid@odata.bind": selectedPriceLevelId ? `/pricelevels(${selectedPriceLevelId})` : "",
"bt_Program@odata.bind": selectedProgram ? `/mshied_programs(${selectedProgram})` : "",
"bt_GradeLevel@odata.bind": selectedGradeLevel ? `/mshied_programlevels(${selectedGradeLevel})` : "",
"btfh_Intake@odata.bind": selectedIntake ? `/btfh_intakes(${selectedIntake})` : ""
};
const shipWarehouseBind = isGuid(shipWarehouseId) ? `/msdyn_warehouses(${shipWarehouseId})` : "(not set)";
const shipSiteBind = isGuid(shipSiteId) ? `/msdyn_operationalsites(${shipSiteId})` : "(not set)";
const lines = [];
const push = (k, v) => lines.push(`${eH(k)}: ${eH(v)}
`);
push("StudentId", studentId || "");
push("OpportunityId (current)", opportunityId || "");
push("Student Name", `${data.firstname || ""} ${data.lastname || ""}`.trim());
push("Student Number", data.msdyn_contactpersonid || "");
push("Preferred Campus", data["_btfh_preferredcampus_value@OData.Community.Display.V1.FormattedValue"] || "");
push("Preferred Campus Id", selectedCampus || "");
push("Shipping Warehouse Id", shipWarehouseId || "");
push("Shipping Warehouse bind", shipWarehouseBind);
push("Shipping Site Id", shipSiteId || "");
push("Shipping Site bind", shipSiteBind);
push("Selected Intake", selectedIntakeText || "");
push("Selected Intake Id", selectedIntake || "");
push("Intake Year", selectedIntakeYear || "");
push("Intake Date", btfhTargetIntakeDate || "");
push("Program Id", selectedProgram || "");
push("Program Version Id", selectedProgramVersion || "");
push("Grade Level Id", selectedGradeLevel || "");
push("Price List Id", selectedPriceLevelId || "");
push("Payment Selection", String(selectedPaymentValue || ""));
lines.push("
Opportunity payload preview
");
Object.keys(dataObject).forEach(k => {
const v = dataObject[k];
if (v === "") return;
push(k, v);
});
return lines.join("");
} catch (err) {
console.error("buildOpportunityRequirementInfoHtml failed", err);
return "Submission details are not available yet. Please try again in a moment.";
}
}
function extractGuidFromEntityIdHeader(h) {
const s = (h || "").toString();
const m = s.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
return m ? m[0] : "";
}
function showSubmissionFailedModal(err) {
const title = "We couldn't submit your enrolment";
const msg =
"Your submission could not be completed. Please try again. " +
"If this continues, contact your Student Advisor via " +
'Connect with Student Advisor.';
try {
if (typeof showInfoModal === "function") {
showInfoModal("submission-failed-modal", title, msg);
return;
}
} catch (e) { }
alert("Your submission could not be completed. Please try again or contact a Student Advisor.");
}
function showOpportunitySyncSummaryModal(summary, oppId, studId) {
try {
const existing = document.getElementById("opportunity-sync-summary-modal");
if (existing) existing.remove();
} catch (e) { }
const s = summary || {};
const added = Number.isFinite(s.added) ? s.added : 0;
const removed = Number.isFinite(s.removed) ? s.removed : 0;
const changed = !!s.changed;
const q = s.quoteInfo || {};
const deletedQuotes = Number.isFinite(q.deletedQuotes) ? q.deletedQuotes : 0;
const activeQuotes = Number.isFinite(q.activeQuotes) ? q.activeQuotes : 0;
const totalQuotes = Number.isFinite(q.totalQuotes) ? q.totalQuotes : 0;
const oid = (oppId || "").toString().trim();
const sid = (studId || "").toString().trim();
const quoteUrl = (oid && sid) ? `/selfservice/quotecreate?opportunityid=${oid}&contactid=${sid}` : "#";
const title = "Submission ready";
const msg =
(changed
? "Your module selection was synced to your application."
: "No changes were required for your module selection.") +
"
" +
`Opportunity products added: ${added}
` +
`Opportunity products removed: ${removed}
` +
`Quotes found: ${totalQuotes}
` +
`Active quotes deleted: ${deletedQuotes}${activeQuotes && deletedQuotes !== activeQuotes ? ` (attempted ${activeQuotes})` : ""}
` +
"
Next step: open your quote page to generate a fresh quote from the updated opportunity.";
const html = ``;
document.body.insertAdjacentHTML("beforeend", html);
const root = document.getElementById("opportunity-sync-summary-modal");
const okBtn = document.getElementById("oss-ok");
const quoteBtn = document.getElementById("oss-open-quote");
if (okBtn) okBtn.onclick = function () { try { root.remove(); } catch (e) { } };
if (quoteBtn) quoteBtn.onclick = function (e) {
e.preventDefault();
if (typeof goToQuote === "function") {
goToQuote(oppId, studId);
} else if (quoteUrl && quoteUrl !== "#") {
window.location.href = quoteUrl;
}
};
}
async function showOpportunityRequirementInfo() {
try {
const html = await buildOpportunityRequirementInfoHtml();
if (typeof showInfoModal === "function") {
showInfoModal("submission-requirements-modal", "Submission details", html);
return;
}
alert("Submission details are not available (modal not loaded).");
} catch (err) {
console.error("showOpportunityRequirementInfo failed", err);
try {
if (typeof showInfoModal === "function") {
showInfoModal(
"submission-requirements-error-modal",
"Submission details",
"We couldn't display submission details right now. Please try again."
);
return;
}
} catch (e) { }
alert("We couldn't display submission details right now. Please try again.");
}
}
function updateOpportunity() {
if (window.BLOCKED_BY_INTEGRATED_QUOTE) return;
setStep(3);
showProgressModal();
setProgress(5, "Preparing opportunity...");
$("#updateButton").hide();
const selectedPaymentValue = MONTHLY_OPTION_VALUE;
const selectedPriceLevelId = MonthlyPriceList;
if (!selectedPriceLevelId) {
alert("Price details are not loaded yet. Click 'Load Modules' first, then try again. If this continues, please contact your Student Advisor.");
$("#updateButton").show();
hideProgressModal();
return;
}
checkForExistingOpportunity(studentId).then(async (result) => {
opportunityId = result;
const canProceed = await guardOpportunityWithQuote(opportunityId, studentId);
if (!canProceed) { hideProgressModal(); notificationMsg.hide(); return; }
submitOpportunity();
notificationMsg.show("Please wait while your modules are being updated.");
try {
if (!campusTable || !campusTable.length) {
await populateCampusTable();
}
} catch (e) {
console.warn("Campus mapping load failed", e);
}
const verb = opportunityId ? "PATCH" : "POST";
setProgress(10, verb === "POST" ? "Creating opportunity..." : "Updating opportunity...");
const selectedProgram = btfhCurrentProgram;
const selectedProgramVersion = btfhTargetProgamVersion;
const selectedGradeLevel = ContactData._mshied_currentprogramlevelid_value;
const selectedCampus = ContactData._btfh_preferredcampus_value;
const selectedIntake = $("#btfh_intake option:selected").val();
let selectedIntakeYear = "";
if (selectedIntake) selectedIntakeYear = extractIntakeYear($("#btfh_intake option:selected").text()) || "";
const foundIntakeYear = (intakeDict.results || []).find(x => x.Year === selectedIntakeYear);
const btfhTargetIntakeDate = foundIntakeYear ? foundIntakeYear.Date : null;
const campusValue = ContactData._btfh_preferredcampus_value;
const ship = await resolveCampusShippingIds(campusValue);
const shipWarehouseId = ship && ship.shippingWarehouseId;
const shipSiteId = ship && ship.shippingSiteId;
if (!isGuid(shipWarehouseId) || !isGuid(shipSiteId)) {
hideProgressModal();
$("#updateButton").show();
notificationMsg.hide();
showCampusShippingMissingModal(campusValue, shipWarehouseId, shipSiteId);
return;
}
const dataObject = {
"parentcontactid@odata.bind": `/contacts(${studentId})`,
"btfh_saletype": 948780002, "statecode": 0, "statuscode": 948780003,
"btfh_newstatusreasons": 948780010, "btfh_applicationstatus": 948780002,
"edv_pricelistselection": selectedPaymentValue,
"btfh_enrollmenttype": 948780004, "btfh_studentnumber": ContactData.btfh_studentnumber,
"btfh_firstname": ContactData.firstname, "btfh_lastname": ContactData.lastname,
"btfh_studenttype": 948780001, "btfh_enrolmentstatus": false,
"btfh_academicvettedstatus": 948780005, "btfh_academicallyvetted": false,
"btfh_RegistrationCampus@odata.bind": `/btfh_campuses(${selectedCampus})`,
"btfh_PreferredCampus@odata.bind": `/btfh_campuses(${selectedCampus})`,
"btfh_SecondPreferredCampus@odata.bind": `/btfh_campuses(${selectedCampus})`,
"btjg_intakeyear": selectedIntakeYear, "btfh_intakedate": btfhTargetIntakeDate,
"bt_ProgramVersion@odata.bind": `/mshied_programversions(${selectedProgramVersion})`,
"pricelevelid@odata.bind": `/pricelevels(${selectedPriceLevelId})`,
"bt_Program@odata.bind": `/mshied_programs(${selectedProgram})`,
"bt_GradeLevel@odata.bind": `/mshied_programlevels(${selectedGradeLevel})`,
"btfh_Intake@odata.bind": `/btfh_intakes(${selectedIntake})`
};
dataObject["btfh_ShippingWarehouse@odata.bind"] = `/msdyn_warehouses(${shipWarehouseId})`;
dataObject["btfh_ShippingSite@odata.bind"] = `/msdyn_operationalsites(${shipSiteId})`;
if (!opportunityId) {
dataObject["name"] = `Returning - ${ContactData.firstname} ${ContactData.lastname}`;
}
try {
if (verb === "POST") {
const xhr = await webapi.safeAjax({
type: "POST",
url: "https://www.vossie.net/_api/opportunities",
contentType: "application/json",
data: JSON.stringify(dataObject),
timeout: 90000,
returnXHR: true
});
const entityHeader = (xhr && xhr.getResponseHeader)
? (xhr.getResponseHeader("entityid") || xhr.getResponseHeader("OData-EntityId") || xhr.getResponseHeader("Location") || xhr.getResponseHeader("location"))
: "";
const entityId = extractGuidFromEntityIdHeader(entityHeader) || (entityHeader || "").toString().trim();
if (entityId) opportunityId = entityId;
if (!validateOpportunityId(opportunityId)) {
throw new Error("Opportunity created but id could not be resolved");
}
setProgress(35, "Saving price list & campus...");
setProgress(55, "Syncing modules with your application...");
const syncSummary = await updateOpportunityProducts(opportunityId, selectedPriceLevelId);
$("#quoteref")
.attr("href", `/selfservice/quotecreate?opportunityid=${opportunityId}&contactid=${studentId}`)
.show();
setProgress(100, "Done");
setStep(4);
hideProgressModal();
showOpportunitySyncSummaryModal(syncSummary, opportunityId, studentId);
} else {
setProgress(55, "Syncing modules with your application...");
const syncSummary = await updateOpportunityProducts(opportunityId, selectedPriceLevelId);
$("#quoteref")
.attr("href", `/selfservice/quotecreate?opportunityid=${opportunityId}&contactid=${studentId}`)
.show();
setProgress(100, "Done");
setStep(4);
hideProgressModal();
notificationMsg.hide();
showOpportunitySyncSummaryModal(syncSummary, opportunityId, studentId);
}
} catch (err) {
console.error("Opportunity save failed", err);
setProgress(100, "Something went wrong - please try again");
hideProgressModal();
$("#updateButton").show();
notificationMsg.hide();
showSubmissionFailedModal(err);
}
});
}
function patchOpportunityAmount(oid, amt) {
let d = { "totalamount": amt, "totallineitemamount": amt };
webapi.safeAjax({
type: "PATCH", url: `https://www.vossie.net/_api/opportunities(${oid})`,
contentType: "application/json", data: JSON.stringify(d), timeout: 30000,
success: function () { console.log("Campus/payment saved"); },
error: function (x, t, e) { console.error("Error in opportunity patch:", t, e); }
});
}
function patchOpportunity(oid, plid) {
const c = ContactData._btfh_preferredcampus_value;
let d = {
"btfh_PreferredCampus@odata.bind": `/btfh_campuses(${c})`,
"pricelevelid@odata.bind": `/pricelevels(${plid})`
};
webapi.safeAjax({
type: "PATCH", url: `https://www.vossie.net/_api/opportunities(${oid})`,
contentType: "application/json", data: JSON.stringify(d), timeout: 30000,
success: function () { console.log("Campus/payment saved"); },
error: function (x, t, e) { console.error("Unable to save registration:", t, e); }
});
}
function extractIntakeYear(txt) {
const r = new RegExp("(\\d{4})"); const m = r.exec(txt); if (m && m.length > 1) return m[1]; return "";
}
function getSelectedIntakeYearNumber() {
try {
const y = (typeof getSelectedIntakeYear === "function") ? getSelectedIntakeYear() : null;
if (Number.isFinite(y) && y >= 1900) return y;
} catch (e) { }
const txt = $("#btfh_intake option:selected").text() || "";
const yn = Number(extractIntakeYear(txt));
return Number.isFinite(yn) && yn >= 1900 ? yn : null;
}
function getProductIdFromAny(m) {
return (m && (m.ProductId || m.productid || m.productId || m.ProductID)) || null;
}
function pickAdminFeeModuleFrom(list, targetYear) {
const mods = Array.isArray(list) ? list : [];
const isAdmin = m => (typeof isAdminModuleLocal === "function")
? isAdminModuleLocal(m)
: (typeof isAdminModule === "function" ? isAdminModule(m) : false);
const candidates = mods.filter(m => isAdmin(m) && getProductIdFromAny(m));
if (!candidates.length) return null;
const ty = Number.isFinite(targetYear) && targetYear >= 1900 ? targetYear : null;
if (ty != null && typeof getIntakeYearForModule === "function") {
const exact = candidates.filter(m => getIntakeYearForModule(m) === ty);
if (exact.length) return exact[0];
}
candidates.sort((a, b) => {
const aCode = String(a.ProductCode || a.ModuleCode || a.ProductNumber || "");
const bCode = String(b.ProductCode || b.ModuleCode || b.ProductNumber || "");
return aCode.localeCompare(bCode);
});
return candidates[0];
}
function findAdminFeeModuleForSelectedYear() {
const y = getSelectedIntakeYearNumber();
const sources = [];
if (typeof futurePlannerData !== "undefined" && futurePlannerData && Array.isArray(futurePlannerData.results) && futurePlannerData.results.length) {
sources.push(futurePlannerData.results);
}
if (typeof qualPlannerData !== "undefined" && qualPlannerData && Array.isArray(qualPlannerData.results) && qualPlannerData.results.length) {
sources.push(qualPlannerData.results);
}
if (typeof qualProgData !== "undefined" && Array.isArray(qualProgData) && qualProgData.length) {
sources.push(qualProgData);
}
for (const src of sources) {
const found = pickAdminFeeModuleFrom(src, y);
if (found) return found;
}
return null;
}
async function updateOpportunityProducts(opportunityId, pricelevelid) {
await validateOpportunityId(opportunityId);
const data = await fetchOpportunityProducts(opportunityId);
const current = extractOpportunityProducts(data);
const oppProdIdsByProdId = new Map();
current.forEach(x => {
if (!x || !x.productid || !x.opportunityproductid) return;
const pid = normalizeGuid(x.productid);
if (!pid) return;
if (!oppProdIdsByProdId.has(pid)) oppProdIdsByProdId.set(pid, []);
oppProdIdsByProdId.get(pid).push(x.opportunityproductid);
});
const currentIds = new Set(Array.from(oppProdIdsByProdId.keys()));
const getWantedKey = (m) => {
const raw = (m && (m.ModuleCode || m.ProductCode || m.ProductNumber)) || "";
if (!raw) return "";
try {
if (typeof getBaseModuleCode === "function") {
return String(getBaseModuleCode(raw) || "").toUpperCase().trim();
}
} catch (e) { }
return String(raw).split("-")[0].toUpperCase().trim();
};
const getExactCode = (m) => {
const raw = (m && (m.ProductCode || m.ModuleCode || m.ProductNumber)) || "";
return String(raw || "").toUpperCase().trim();
};
const coreExact = (outStandingModules?.unregisteredCore || []).map(i => getExactCode(i)).filter(Boolean);
const coreRoots = (outStandingModules?.unregisteredCore || []).map(i => getWantedKey(i)).filter(Boolean);
const registeredCoreExact = (outStandingModules?.registeredCore || []).map(i => getExactCode(i)).filter(Boolean);
const registeredCoreRoots = (outStandingModules?.registeredCore || []).map(i => getWantedKey(i)).filter(Boolean);
const repeatExact = (outStandingModules?.repeater || []).map(i => getExactCode(i)).filter(Boolean);
const repeatRoots = (outStandingModules?.repeater || []).map(i => getWantedKey(i)).filter(Boolean);
const electiveCheckedRaw = (outStandingModules?.elective || [])
.map(i => (i && (i.ProductCode || i.ModuleCode || i.ProductNumber)) || "")
.filter(v => !!v && $(`input[type="checkbox"][value="${v}"]`).is(":checked"));
const electiveExactChecked = electiveCheckedRaw.map(v => String(v).toUpperCase().trim()).filter(Boolean);
const electiveRootsChecked = electiveCheckedRaw.map(v => getWantedKey({ ModuleCode: v })).filter(Boolean);
const wantedExact = new Set([
...coreExact,
...repeatExact,
...electiveExactChecked,
...registeredCoreExact,
]);
const wantedRoots = new Set([
...coreRoots,
...repeatRoots,
...electiveRootsChecked,
...registeredCoreRoots,
]);
const adminFee = findAdminFeeModuleForSelectedYear();
const adminPid = getProductIdFromAny(adminFee);
const adminCode = adminFee && (adminFee.ProductCode || adminFee.ModuleCode || adminFee.ProductNumber);
if (adminCode) {
const exact = getExactCode({ ModuleCode: adminCode });
const root = getWantedKey({ ModuleCode: adminCode });
if (exact) wantedExact.add(exact);
if (root) wantedRoots.add(root);
}
let desiredIds = [...new Set(
(qualProgData || [])
.filter(p => (wantedExact.has(getExactCode(p)) || wantedRoots.has(getWantedKey(p))) && getProductIdFromAny(p))
.map(p => normalizeGuid(getProductIdFromAny(p)))
.filter(Boolean)
)];
const desiredSet = new Set(desiredIds);
if (adminPid) desiredSet.add(normalizeGuid(adminPid));
desiredIds = [...desiredSet].filter(Boolean);
const knownPlannerIds = new Set((qualProgData || []).map(p => normalizeGuid(getProductIdFromAny(p))).filter(Boolean));
if (adminPid) knownPlannerIds.add(normalizeGuid(adminPid));
if (!adminPid) {
console.warn("Admin fee product not found for selected intake year.");
}
const toDeleteSet = new Set();
oppProdIdsByProdId.forEach((oppIds, pid) => {
if (!pid || !Array.isArray(oppIds) || !oppIds.length) return;
if (knownPlannerIds.has(pid) && !desiredSet.has(pid)) {
oppIds.forEach(id => { if (id) toDeleteSet.add(id); });
return;
}
if (desiredSet.has(pid) && oppIds.length > 1) {
oppIds.slice(1).forEach(id => { if (id) toDeleteSet.add(id); });
}
});
const toDelete = [...toDeleteSet];
const toAdd = desiredIds.filter(id => !currentIds.has(normalizeGuid(id)));
await deleteOpportunityProducts(toDelete);
setProgress(70, "Removing old modules...");
await addOpportunityProductsByProductIds(toAdd, opportunityId, pricelevelid);
setProgress(85, "Adding selected modules...");
const changed = toDelete.length > 0 || toAdd.length > 0;
let quoteInfo = { foundQuotes: false, totalQuotes: 0, activeQuotes: 0, deletedQuotes: 0 };
if (changed) {
setProgress(80, "Deleting prior quotes...");
quoteInfo = await deleteExistingQuotes(opportunityId);
setProgress(85, "Completed finalising prior quotes...");
};
setProgress(92, "Refreshing quote...");
return { changed, added: toAdd.length, removed: toDelete.length, quoteInfo };
}
function validateOpportunityId(oid) { const guidRegex = new RegExp("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); return oid && guidRegex.test(oid); }
function fetchOpportunityProducts(oid) { const url = `https://www.vossie.net/_api/opportunityproducts?$filter=_opportunityid_value eq ${oid}`; const headers = { "Accept": "application/json", "Content-Type": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" }; return webapi.safeAjax({ url, type: "GET", headers }); }
function extractOpportunityProducts(data) { return data.value.map(o => ({ opportunityproductid: o.opportunityproductid, productid: o._productid_value })); }
function deleteOpportunityProducts(oppProdIds) { const reqs = oppProdIds.map(id => { const url = `https://www.vossie.net/_api/opportunityproducts(${id})`; const headers = { "Accept": "application/json", "Content-Type": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" }; return webapi.safeAjax({ url, type: "DELETE", headers }); }); return Promise.all(reqs); }
function getShippingWarehouse(v) {
const c = findCampusRowById(v);
const wh = c ? normalizeGuid(c._btfh_shippingwarehouse_value) : "";
return isGuid(wh) ? wh : null;
}
function getShippingSite(v) {
const c = findCampusRowById(v);
const st = c ? normalizeGuid(c._btfh_shippingsite_value) : "";
return isGuid(st) ? st : null;
}
async function addOpportunityProductsByProductIds(productIds, oid, plid) {
patchOpportunityAmount(oid, 0);
if (!Array.isArray(productIds) || !productIds.length) {
return;
}
const campusId = ContactData._btfh_preferredcampus_value || "";
const ship = await resolveCampusShippingIds(campusId);
const shipWarehouseId = ship && ship.shippingWarehouseId;
const shipSiteId = ship && ship.shippingSiteId;
if (!isGuid(shipWarehouseId) || !isGuid(shipSiteId)) {
showCampusShippingMissingModal(campusId, shipWarehouseId, shipSiteId);
throw new Error("Missing campus shipping site/warehouse");
}
const selIntakeText = $("#btfh_intake option:selected").text() || "";
const selIntakeYear = extractIntakeYear(selIntakeText) || new Date().getFullYear().toString();
const intakeRow = (intakeDict.results || []).find(x => x.Year === selIntakeYear);
const btIntakeId = intakeRow ? intakeRow.Id : ($("#btfh_intake").val() || "");
const planner = (typeof futurePlannerData !== "undefined" && futurePlannerData && futurePlannerData.results && futurePlannerData.results.length)
? futurePlannerData
: (typeof qualPlannerData !== "undefined" ? qualPlannerData : null);
const plannerList = (planner && planner.results) ? planner.results : [];
const payload = productIds
.filter(pid => pid)
.map(pid => {
const pl = qualProgData.find(pp => pp.ProductId === pid) || plannerList.find(pm => getProductIdFromAny(pm) === pid) || {};
return {
opportunityid: String(oid || ""),
productid: String(pid),
productname: pl.Name || "",
pricelevelid: plid || "",
bt_GradeLevel: ContactData._mshied_currentprogramlevelid_value || "",
bt_Program: btfhCurrentProgram || "",
bt_ProgramVersion: btfhTargetProgamVersion || "",
bt_Intake: btIntakeId || "",
quantity: 1.0,
uomid: "9e495116-809b-eb11-b1ac-000d3ab42a69",
transactioncurrencyid: "9ebe5126-2d84-eb11-b1ab-000d3ab8569d",
msdyn_ShippingWarehouse: shipWarehouseId,
msdyn_ShippingSite: shipSiteId,
bt_yearofundertaking: Number(pl.YearOfUnderTaking || selIntakeYear) + 206437979
};
});
try {
await SubmitToFlow(JSON.stringify(payload));
} catch (e) {
const msg = (e && e.message) ? e.message : String(e || "");
if (msg && msg.toLowerCase().includes("submission processed")) {
return;
}
throw e;
}
}
function onVersionChange() {
(async function () {
let v = btfhCurrentProgram, i = $("#btfh_intake option:selected").val();
if (v) {
document.getElementById('coreregistered-container').innerHTML = "";
document.getElementById('coreunregistered-container').innerHTML = "";
document.getElementById('elective-container').innerHTML = "";
document.getElementById('repeater-container').innerHTML = "";
coreIds.clear(); repeaterIds.clear(); electiveIds.clear();
}
try {
await ensurePriceLists(btfhCurrentProgramVersion, i || btfhCurrentIntake);
if (typeof checkAndUpdatePriceLevelIfNeeded === "function") {
await checkAndUpdatePriceLevelIfNeeded();
}
} catch (e) {
console.warn("onVersionChange price list refresh failed", e);
}
})();
}
function setIntakeValue(primaryValue) {
var selVal = $("#btfh_intake").val(), updated = false;
if (primaryValue) {
$.getJSON(`/fetchintake/?id=${primaryValue}`, function (mata) {
intakeDict = mata;
if (!updated && mata.results.length > 0) {
updated = true; var cur = $("#btfh_intake").val() || [];
$("#btfh_intake option").each(function () {
var v = $(this).val();
if (cur.indexOf(v) === -1) $(this).remove();
});
mata.results.forEach(e => {
let o = document.createElement("option"); o.value = e.Id; o.innerText = e.RBName; $("#btfh_intake").append(o);
});
if (cur.includes(selVal)) $("#btfh_intake").val(selVal); else $("#btfh_intake").val([]);
} else {
alert("We couldn't load the available intakes. Please contact your Student Advisor so we can help you continue your enrolment.");
}
});
} else $("#btfh_intake").val([]);
}
function submitOpportunity() {
var checkedElectives = [], hasCheckboxes = false;
$('input[type="checkbox"]').each(function () {
hasCheckboxes = true; if (this.checked) checkedElectives.push(this.value);
});
}
function displayElectiveInstructions(pvid) {
if (!programVersionsDict?.results) return;
const mv = programVersionsDict.results.find(v => v.Id?.toLowerCase() === pvid?.toLowerCase());
if (mv && mv.ElectiveInstructions) document.getElementById("elective-instructions").innerHTML = mv.ElectiveInstructions;
else document.getElementById("elective-instructions").innerHTML = "";
if (mv && mv.ElectiveInstructions) document.getElementById("elective-instructions2").innerHTML = mv.ElectiveInstructions;
else document.getElementById("elective-instructions2").innerHTML = "";
}
function appendAppliedModuleRulesToInstructions() {
try {
const log = window._appliedModuleRulesLog;
if (!Array.isArray(log) || !log.length) return;
const target1 = document.getElementById("elective-instructions");
const target2 = document.getElementById("elective-instructions2");
if (!target1 && !target2) return;
const msgs = (log || [])
.filter(r => r && (r.type === 948781000 || r.type === 948781002 || r.type === 948781007))
.map(r => {
const name = r.name || "Module rule";
const desc = r.description && r.description !== name ? ` - ${r.description}` : "";
return `${name}${desc}`;
})
.filter(Boolean);
if (!msgs.length) return;
const html =
'Module rules applied:' +
msgs.map(m => `- ${m}
`).join("") +
"
";
[target1, target2].forEach(t => {
if (!t) return;
const existing = t.querySelector(".module-rules-applied");
if (existing) existing.remove();
t.innerHTML += html;
});
} catch (e) {
console.warn("Unable to append applied module rules to elective instructions:", e);
}
}