diff --git a/app/controllers/main_routes/laborStatusForm.py b/app/controllers/main_routes/laborStatusForm.py
index dc794952d..3f050938a 100755
--- a/app/controllers/main_routes/laborStatusForm.py
+++ b/app/controllers/main_routes/laborStatusForm.py
@@ -14,7 +14,7 @@
from flask import json, jsonify
from flask import request
from datetime import datetime, date, timedelta
-from flask import Flask, redirect, url_for, flash
+from flask import Flask, redirect, url_for, flash, g
from app.logic.emailHandler import*
from app.logic.userInsertFunctions import*
from app.models.supervisor import Supervisor
@@ -22,6 +22,7 @@
from app.controllers.main_routes.laborReleaseForm import createLaborReleaseForm
from app.logic.allPendingForms import saveStatus
from app.logic.statusFormFunctions import *
+from app.logic.allocation import getBandAllocationStatus, getAllocationWarning
@main_bp.route('/laborstatusform', methods=['GET'])
@@ -170,6 +171,41 @@ def checkTotalHours(termCode, student, hours):
totalHours = totalHours + int(hours)
return json.dumps(totalHours)
+@main_bp.route("/laborstatusform/checkallocation", methods=["GET"])
+def checkAllocation():
+ """ Checks the department's allocation status for the hour-band being submitted. """
+ departmentOrg = request.args.get("departmentOrg")
+ departmentAcct = request.args.get("departmentAcct")
+ jobType = request.args.get("jobType")
+ hours = request.args.get("hours")
+
+ dept = Department.get_or_none(Department.ORG == departmentOrg, Department.ACCOUNT == departmentAcct)
+ if not dept:
+ return jsonify({"error": "Department not found"}), 404
+
+ status = getBandAllocationStatus(dept, g.openTerm, jobType, int(hours))
+ if status is None:
+ return jsonify({"error": "No allocation data for this job type/hours band"}), 404
+
+ return jsonify(status)
+
+@main_bp.route("/laborstatusform/allocationsummary", methods=["GET"])
+def allocationSummary():
+ """ Returns the department's current total-positions and break-hours allocation status, so the
+ labor status form can show a live summary that updates as students are added before submission. """
+ departmentOrg = request.args.get("departmentOrg")
+ departmentAcct = request.args.get("departmentAcct")
+
+ dept = Department.get_or_none(Department.ORG == departmentOrg, Department.ACCOUNT == departmentAcct)
+ if not dept:
+ return jsonify({"error": "Department not found"}), 404
+
+ warning = getAllocationWarning(dept, g.openTerm)
+ if warning is None:
+ return jsonify({"error": "No allocation data for this department"}), 404
+
+ return jsonify(warning)
+
@main_bp.route("/laborStatusForm/modal/releaseAndRehire", methods=['POST'])
def releaseAndRehire():
try:
diff --git a/app/logic/allPendingForms.py b/app/logic/allPendingForms.py
index a9d4a3bd2..f93af930d 100644
--- a/app/logic/allPendingForms.py
+++ b/app/logic/allPendingForms.py
@@ -1,6 +1,6 @@
import json
from datetime import date
-from flask import jsonify
+from flask import jsonify, g, flash
from app.models.formHistory import FormHistory
from app.models.status import Status
from app.logic.banner import Banner
@@ -14,9 +14,11 @@
from app.models.overloadForm import OverloadForm
from app.models.notes import Notes
from app.login_manager import DoesNotExist, render_template
+from app.logic.allocation import getAllocationWarning
def saveStatus(new_status, formHistoryIds, currentUser):
+ approvedDepartments = {}
try:
if new_status == 'Denied by Admin':
# Index 1 will always hold the reject reason in the list, so we can
@@ -66,6 +68,8 @@ def saveStatus(new_status, formHistoryIds, currentUser):
email.laborStatusFormRejected()
if new_status == "Approved" and formType == "Labor Status Form":
email.laborStatusFormApproved()
+ dept = formHistory.formID.department
+ approvedDepartments[dept.departmentID] = dept
if new_status == "Approved" and formType == "Labor Adjustment Form":
# This function is triggered whenever an adjustment form is approved.
# The following function overrides the original data in lsf with the new data from adjustment form.
@@ -80,6 +84,16 @@ def saveStatus(new_status, formHistoryIds, currentUser):
print("Error preparing form for status update:", e)
return jsonify({"success": False}), 500
+ # After approving, let the admin know right away if any affected department
+ # is now over its allocated positions or break hours (informational only).
+ for dept in approvedDepartments.values():
+ warning = getAllocationWarning(dept, g.openTerm)
+ if warning and warning['isOverAllocated']:
+ messageParts = [f"{b['label']} ({b['used']}/{b['allocated']})" for b in warning['overAllocatedBands']]
+ if warning['isBreakHoursOverAllocated']:
+ messageParts.append(f"break hours ({warning['breakHoursUsed']}/{warning['breakHoursAllocated']})")
+ flash(f"{dept.DEPT_NAME} is now over its allocation for: {', '.join(messageParts)}.", "warning")
+
return jsonify({"success": True})
def overrideOriginalStatusFormOnAdjustmentFormApproval(form, LSF):
@@ -196,9 +210,8 @@ def laborAdminOverloadApproval(rsp, historyForm, status, currentUser, currentDat
# extract data from the database to populate pending form approval modal
def modal_approval_and_denial_data(formHistoryIdList):
- ''' This method grabs the data that populated the on approve modal for lsf'''
-
details_list = []
+ allocationWarningsByDept = {}
for fhID in formHistoryIdList:
formHistory = FormHistory.get(FormHistory.formHistoryID == fhID)
lsf = formHistory.formID
@@ -208,7 +221,8 @@ def modal_approval_and_denial_data(formHistoryIdList):
supervisorName = f"{lsf.supervisor.FIRST_NAME} {lsf.supervisor.LAST_NAME}"
weeklyHours = lsf.weeklyHours
contractHours = lsf.contractHours
- deptName = lsf.department.DEPT_NAME
+ dept = lsf.department
+ deptName = dept.DEPT_NAME
if formHistory.adjustedForm:
match formHistory.adjustedForm.fieldAdjusted:
@@ -223,11 +237,17 @@ def modal_approval_and_denial_data(formHistoryIdList):
case "contractHours":
contractHours = formHistory.adjustedForm.newValue
case "department":
- deptName = Department.get(Department.ORG==formHistory.adjustedForm.newValue).DEPT_NAME
+ dept = Department.get(Department.ORG==formHistory.adjustedForm.newValue)
+ deptName = dept.DEPT_NAME
details_list.append([studentName, deptName, position, str(weeklyHours),str(contractHours), supervisorName])
- return details_list
+ if dept.departmentID not in allocationWarningsByDept:
+ warning = getAllocationWarning(dept, g.openTerm)
+ if warning:
+ allocationWarningsByDept[dept.departmentID] = warning
+
+ return {"details": details_list, "allocationWarnings": list(allocationWarningsByDept.values())}
def financialAidSAASOverloadApproval(historyForm, rsp, status, currentUser, currentDate):
diff --git a/app/logic/allocation.py b/app/logic/allocation.py
new file mode 100644
index 000000000..35219eabe
--- /dev/null
+++ b/app/logic/allocation.py
@@ -0,0 +1,139 @@
+from peewee import fn
+
+from app.models.allocation import Allocation
+from app.models.laborStatusForm import LaborStatusForm
+from app.models.formHistory import FormHistory
+from app.models.term import Term
+
+# (Allocation field name, LaborStatusForm.jobType, LaborStatusForm.weeklyHours)
+ALLOCATION_BAND_FIELDS = [
+ ('primary_10', 'Primary', 10),
+ ('primary_12', 'Primary', 12),
+ ('primary_15', 'Primary', 15),
+ ('primary_20', 'Primary', 20),
+ ('secondary_5', 'Secondary', 5),
+ ('secondary_10', 'Secondary', 10),
+]
+
+BAND_LABELS = {fieldName: f"{hours} Hour {jobType}" for fieldName, jobType, hours in ALLOCATION_BAND_FIELDS}
+
+
+def getTotalAllocations(term, dept):
+ """Return the department's allocated totals per band for a term."""
+ if not term or not dept:
+ return None
+
+ allocation = Allocation.get_or_none(Allocation.department == dept, Allocation.termCode == term)
+ if not allocation:
+ return None
+
+ bandTotals = {fieldName: getattr(allocation, fieldName) for fieldName, _, _ in ALLOCATION_BAND_FIELDS}
+ return {
+ "allocation": allocation,
+ "bandTotals": bandTotals,
+ "totalAllocations": sum(bandTotals.values()),
+ }
+
+
+def getContractedAllocations(term, dept):
+ """Return the department's used positions per band and approved break hours for a term."""
+ if not term or not dept:
+ return None
+
+ usedPositions = {}
+ for fieldName, jobType, hours in ALLOCATION_BAND_FIELDS:
+ usedPositions[fieldName] = (
+ LaborStatusForm.select()
+ .join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID))
+ .where(
+ LaborStatusForm.department == dept,
+ LaborStatusForm.termCode == term,
+ LaborStatusForm.jobType == jobType,
+ LaborStatusForm.weeklyHours == hours,
+ FormHistory.historyType == "Labor Status Form",
+ ~(FormHistory.status % "Denied%"),
+ )
+ .distinct()
+ .count()
+ )
+
+ # Break hours are tracked on separate break-term rows (e.g. Thanksgiving Break)
+ # that share the same academic year prefix as the given AY term.
+ yearPrefix = str(term.termCode)[:-2]
+ breakTermCodes = [
+ t.termCode for t in Term.select().where(Term.isBreak == True)
+ if str(t.termCode).startswith(yearPrefix)
+ ]
+ breakHours = (
+ LaborStatusForm.select(fn.SUM(LaborStatusForm.contractHours))
+ .join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID))
+ .where(
+ LaborStatusForm.department == dept,
+ LaborStatusForm.termCode.in_(breakTermCodes),
+ FormHistory.historyType == "Labor Status Form",
+ ~(FormHistory.status % "Denied%"),
+ )
+ .scalar()
+ ) or 0
+
+ return {
+ "usedPositions": usedPositions,
+ "usedTotal": sum(usedPositions.values()),
+ "breakHours": breakHours,
+ }
+
+
+def getBandAllocationStatus(dept, term, jobType, hours):
+ fieldName = next((f for f, j, h in ALLOCATION_BAND_FIELDS if j == jobType and h == hours), None)
+ if not fieldName:
+ return None
+
+ totals = getTotalAllocations(term, dept)
+ if not totals:
+ return None
+ contracted = getContractedAllocations(term, dept)
+
+ allocated = totals["bandTotals"][fieldName]
+ used = contracted["usedPositions"][fieldName]
+ return {
+ 'label': BAND_LABELS[fieldName],
+ 'used': used,
+ 'allocated': allocated,
+ 'remaining': allocated - used,
+ 'isOverAllocated': used > allocated,
+ }
+
+
+def getAllocationWarning(dept, term):
+ totals = getTotalAllocations(term, dept)
+ if not totals:
+ return None
+ contracted = getContractedAllocations(term, dept)
+
+ positionsRemaining = totals["totalAllocations"] - contracted["usedTotal"]
+ breakHoursRemaining = totals["allocation"].breakHours - contracted["breakHours"]
+
+ # A department can be within its total position count while still exceeding
+ # one specific hour-band (e.g. over on 10-hour Primary but under on others),
+ # so each band needs to be checked individually, not just the aggregate total.
+ overAllocatedBands = [
+ {'label': BAND_LABELS[fieldName], 'used': contracted["usedPositions"][fieldName], 'allocated': totals["bandTotals"][fieldName]}
+ for fieldName, _, _ in ALLOCATION_BAND_FIELDS
+ if contracted["usedPositions"][fieldName] > totals["bandTotals"][fieldName]
+ ]
+ isPositionsOverAllocated = positionsRemaining < 0 or bool(overAllocatedBands)
+ isBreakHoursOverAllocated = breakHoursRemaining < 0
+
+ return {
+ 'departmentName': dept.DEPT_NAME,
+ 'totalPositionsAllocated': totals["totalAllocations"],
+ 'totalPositionsUsed': contracted["usedTotal"],
+ 'positionsRemaining': positionsRemaining,
+ 'isPositionsOverAllocated': isPositionsOverAllocated,
+ 'overAllocatedBands': overAllocatedBands,
+ 'breakHoursAllocated': totals["allocation"].breakHours,
+ 'breakHoursUsed': contracted["breakHours"],
+ 'breakHoursRemaining': breakHoursRemaining,
+ 'isBreakHoursOverAllocated': isBreakHoursOverAllocated,
+ 'isOverAllocated': isPositionsOverAllocated or isBreakHoursOverAllocated,
+ }
diff --git a/app/models/positionHistory.py b/app/models/positionHistory.py
index 9a248aacb..e9b4e0c2f 100644
--- a/app/models/positionHistory.py
+++ b/app/models/positionHistory.py
@@ -12,4 +12,3 @@ class PositionHistory(baseModel):
class Meta:
indexes = ( (('positionCode', 'revisionDate', 'status'), True), )
-
diff --git a/app/static/css/laborStatusForm.css b/app/static/css/laborStatusForm.css
index c87073a9a..a1a8271ca 100755
--- a/app/static/css/laborStatusForm.css
+++ b/app/static/css/laborStatusForm.css
@@ -20,13 +20,13 @@
.floatleft {
float: left;
width: 47%;
-height: 310px;
+min-height: 310px;
}
.floatright {
float: right;
width: 47%;
-height: 310px;
+min-height: 310px;
}
#mytable {
@@ -68,7 +68,7 @@ selectpicker, label {
}
#plus {
- text-align: center;
+ text-align: left;
padding-bottom: 30px;
}
diff --git a/app/static/js/allPendingForms.js b/app/static/js/allPendingForms.js
index ae79740e4..8ec4b59cb 100644
--- a/app/static/js/allPendingForms.js
+++ b/app/static/js/allPendingForms.js
@@ -41,6 +41,7 @@ $(document).ready(function() {
// CHECK ALL CHECKBOX ON APPROVE BUTTON
$('#checkAll').change(function(){
$(".approveCheckbox").prop('checked', $(this).prop("checked"));
+ updatePageAllocationWarnings();
});
@@ -53,11 +54,42 @@ $(document).ready(function() {
if ($('.approveCheckbox:checked').length == $('.approveCheckbox').length ){
$("#checkAll")[0].checked = true;
}
+ updatePageAllocationWarnings();
});
});
+// Shows the same per-department allocation warnings directly on the list page as boxes get
+// checked/unchecked, so the admin sees the impact before ever opening the approval modal.
+// Reuses the same endpoint the modal uses, since it already groups by department and only
+// returns warnings for departments represented among the given form IDs.
+function updatePageAllocationWarnings() {
+ if ($('#pageAllocationWarnings').length === 0) {
+ return;
+ }
+
+ var checkedIds = $('.approveCheckbox:checked').map(function() { return this.value; }).get();
+ $('#pageAllocationWarnings').empty();
+
+ if (checkedIds.length === 0) {
+ return;
+ }
+
+ $.ajax({
+ type: "POST",
+ url: "/admin/checkedForms",
+ datatype: "json",
+ data: JSON.stringify(checkedIds),
+ contentType: 'application/json',
+ success: function(response) {
+ if (response) {
+ renderAllocationWarningBoxes(response.allocationWarnings, '#pageAllocationWarnings');
+ }
+ }
+ });
+}
+
var labor_details_ids = []; // for insertApprovals() and final_approval() only
function insertApprovals(laborHistoryId = null) {
@@ -87,8 +119,8 @@ function insertApprovals(laborHistoryId = null) {
contentType: 'application/json',
success: function(response) {
if (response) {
- var returned_details = response;
- updateApproveTableData(returned_details);
+ updateApproveTableData(response.details);
+ updateAllocationWarnings(response.allocationWarnings);
}
}
});
@@ -112,6 +144,45 @@ function updateApproveTableData(returned_details) {
}
}
+// Shows a non-blocking allocation warning per department represented among the
+// selected forms, so admins can see the impact of approval before confirming.
+// Each category (positions / break hours) is highlighted independently, since
+// a department can be over on one and fine on the other. Used both for the approval
+// modal and for the live warnings on the list page itself (see updatePageAllocationWarnings).
+function renderAllocationWarningBoxes(allocationWarnings, targetSelector) {
+ var $target = $(targetSelector);
+ $target.empty();
+ if (!allocationWarnings) { return; }
+ for (var i = 0; i < allocationWarnings.length; i++) {
+ var w = allocationWarnings[i];
+ var boxClass = w.isOverAllocated ? 'alert-warning' : 'alert-info';
+ var overStyle = 'color:#a94442; font-weight:bold;';
+ var normalStyle = 'color:#000000;';
+ var positionsStyle = w.isPositionsOverAllocated ? overStyle : normalStyle;
+ var breakHoursStyle = w.isBreakHoursOverAllocated ? overStyle : normalStyle;
+ var title = w.isOverAllocated ? (w.departmentName + ' Over Allocation Warning') : (w.departmentName + ' Allocation');
+ // A department can look fine in total while one specific hour-band is over,
+ // so call those bands out by name instead of only showing the aggregate.
+ var bandDetail = '';
+ if (w.overAllocatedBands && w.overAllocatedBands.length > 0) {
+ var bandStrings = w.overAllocatedBands.map(function(b) {
+ return b.label + ' (' + b.used + ' used / ' + b.allocated + ' allocated)';
+ });
+ bandDetail = ' Over on: ' + bandStrings.join(', ') + '';
+ }
+ var html = '
';
+ $target.append(html);
+ }
+}
+
+function updateAllocationWarnings(allocationWarnings) {
+ renderAllocationWarningBoxes(allocationWarnings, '#allocationWarnings');
+}
+
$('#approvalModal').on('hidden.bs.modal', function () {// Makes the close functionality work when clicking outside of the modal
approvalModalClose();
@@ -120,6 +191,7 @@ $('#approvalModal').on('hidden.bs.modal', function () {// Makes the close functi
function approvalModalClose(){// on close of approval modal we are clearing the table to prevent duplicate data.
$('#classTableBody').empty();
+ $('#allocationWarnings').empty();
labor_details_ids = [] // emptying the list, becuase otherwise will cause duplicate data.
}
diff --git a/app/static/js/laborStatusForm.js b/app/static/js/laborStatusForm.js
index ff9b252a9..b8e77590b 100755
--- a/app/static/js/laborStatusForm.js
+++ b/app/static/js/laborStatusForm.js
@@ -6,11 +6,13 @@ $(document).ready(function(){
if($("#selectedDepartment").val()){ // prepopulates position on redirect from rehire button and checks whether department is in compliance.
checkCompliance($("#selectedDepartment"));
getDepartment($("#selectedDepartment"));
+ loadAllocationSummary();
}
if($("#jobType").val()){ // fills hours per week selectpicker with correct information from laborstatusform. This is triggered on redirect from form history.
var value = $("#selectedHoursPerWeek").val();
$("#selectedHoursPerWeek").val(value);
fillHoursPerWeek("fillhours");
+ checkAllocation();
}
var cookies = document.cookie;
if (cookies){
@@ -23,6 +25,7 @@ $(document).ready(function(){
$("#selectedSupervisor option[value=" + parsedArrayOfStudentCookies[0].stuSupervisorID + "]").attr('selected', 'selected');
$("#selectedDepartment option[value=\"" + parsedArrayOfStudentCookies[0].stuDepartmentORG + "\"]").attr('selected', 'selected');
getDepartment($("#selectedDepartment"));
+ loadAllocationSummary();
preFilledDate($("#selectedTerm"));
showAccessLevel($("#selectedTerm"));
disableTermSupervisorDept();
@@ -336,6 +339,194 @@ function checkCompliance(obj) {
});
}
+// Checks the department's allocation status for the selected job type/hours band.
+// This is informational only and never blocks or disables form submission.
+function checkAllocation() {
+ $("#allocation-remaining-text").hide();
+ $("#allocation-warning").hide();
+
+ var departmentSelect = $("#selectedDepartment");
+ var departmentOrg = departmentSelect.val();
+ var departmentAcct = departmentSelect.find('option:selected').attr('value-account');
+ var jobType = $("#jobType").val();
+ var hours = $("#selectedHoursPerWeek").val();
+
+ if (!departmentOrg || !jobType || !hours) {
+ return;
+ }
+
+ $.ajax({
+ url: "/laborstatusform/checkallocation",
+ data: {
+ departmentOrg: departmentOrg,
+ departmentAcct: departmentAcct,
+ jobType: jobType,
+ hours: hours
+ },
+ dataType: "json",
+ success: function (response){
+ if (!response) {
+ return;
+ }
+ var remaining = response.remaining >= 0 ? response.remaining : 0;
+ $("#allocation-remaining-text").text(response.label + " Positions: " + response.used + "/" + response.allocated + " used (" + remaining + " remaining)").show();
+ if (response.isOverAllocated) {
+ $("#allocation-warning-text").html("This department is already over its allocation for " + response.label + " positions (" + response.used + "/" + response.allocated + "). You may still submit this form, but please contact the Labor Office.");
+ $("#allocation-warning").show();
+ }
+ },
+ error: function () {
+ // Informational only - if the check fails, just leave the allocation panels hidden.
+ }
+ });
+}
+
+// Live department allocation summary (Total Positions / Break Hours). Loaded once per
+// department selection, then bumped locally by +1/-1 as students are added or removed
+// from the table below, so the supervisor sees the effect immediately without waiting
+// on a server round trip for every add/remove.
+var allocationSummaryState = null;
+
+function clearAllocationSummary() {
+ allocationSummaryState = null;
+ $("#allocationSummaryPositionsAllocated").text("");
+ $("#allocationSummaryPositionsContracted").text("");
+ $("#allocationSummaryBreakHoursAllocated").text("");
+ $("#allocationSummaryBreakHoursContracted").text("");
+ $("#allocation-warning").hide();
+}
+
+function loadAllocationSummary() {
+ var departmentSelect = $("#selectedDepartment");
+ var departmentOrg = departmentSelect.val();
+ var departmentAcct = departmentSelect.find('option:selected').attr('value-account');
+
+ clearAllocationSummary();
+
+ if (!departmentOrg) {
+ return;
+ }
+
+ $.ajax({
+ url: "/laborstatusform/allocationsummary",
+ data: {
+ departmentOrg: departmentOrg,
+ departmentAcct: departmentAcct
+ },
+ dataType: "json",
+ success: function (response) {
+ if (!response || response.error) {
+ return;
+ }
+ allocationSummaryState = {
+ positionsAllocated: response.totalPositionsAllocated,
+ positionsUsed: response.totalPositionsUsed,
+ breakHoursAllocated: response.breakHoursAllocated,
+ breakHoursUsed: response.breakHoursUsed
+ };
+ // Students already staged in the table (e.g. restored from a cookie before this
+ // request returned) aren't reflected in the server totals yet, since they haven't
+ // been submitted. Fold them in so the summary matches what's already on screen.
+ for (var i = 0; i < globalArrayOfStudents.length; i++) {
+ applyAllocationDelta(globalArrayOfStudents[i], 1);
+ }
+ renderAllocationSummary();
+ checkLiveAllocationWarning();
+ },
+ error: function () {
+ // Informational only - if the check fails, just leave the summary cells blank.
+ }
+ });
+}
+
+function renderAllocationSummary() {
+ if (!allocationSummaryState) {
+ return;
+ }
+ var s = allocationSummaryState;
+ $("#allocationSummaryPositionsAllocated").text(s.positionsAllocated);
+ $("#allocationSummaryPositionsContracted").text(s.positionsUsed);
+ $("#allocationSummaryBreakHoursAllocated").text(s.breakHoursAllocated);
+ $("#allocationSummaryBreakHoursContracted").text(s.breakHoursUsed);
+}
+
+// Mutates the running totals only, without touching the DOM. Used both by the live
+// add/remove flow below and to silently fold in students already staged in the table
+// (e.g. restored from a cookie) when the baseline first loads. Returns the break-hours
+// delta actually applied, so callers can decide whether to flash that number too.
+function applyAllocationDelta(studentDict, delta) {
+ if (!allocationSummaryState || !studentDict) {
+ return 0;
+ }
+ allocationSummaryState.positionsUsed += delta;
+ var breakHoursDelta = 0;
+ if (studentDict.isTermBreak) {
+ breakHoursDelta = delta * (parseInt(studentDict.stuContractHours, 10) || 0);
+ allocationSummaryState.breakHoursUsed += breakHoursDelta;
+ }
+ return breakHoursDelta;
+}
+
+// Warns as soon as the students staged in the table (not just what's already saved to the
+// database) would push the department over its allocation. checkAllocation() above only
+// ever sees committed/approved forms, so it never fires for a batch being built up right
+// now in this session - this covers that gap using the live running totals instead.
+function checkLiveAllocationWarning() {
+ if (!allocationSummaryState) {
+ return;
+ }
+ var s = allocationSummaryState;
+ var overPositions = s.positionsUsed > s.positionsAllocated;
+ var overBreakHours = s.breakHoursUsed > s.breakHoursAllocated;
+
+ if (!overPositions && !overBreakHours) {
+ $("#allocation-warning").hide();
+ return;
+ }
+
+ var messages = [];
+ if (overPositions) {
+ messages.push("Total Positions (" + s.positionsUsed + "/" + s.positionsAllocated + ")");
+ }
+ if (overBreakHours) {
+ messages.push("Break Hours (" + s.breakHoursUsed + "/" + s.breakHoursAllocated + ")");
+ }
+ $("#allocation-warning-text").html("The students added so far put this department over its allocation for " +
+ messages.join(" and ") + ". You may still continue, but please contact the Labor Office.");
+ $("#allocation-warning").show();
+}
+
+var allocationFlashTimeoutId = null;
+
+// delta is +1 when a student is added to the table, -1 when a row is removed. On an add,
+// flashes " +1" immediately so the supervisor sees the click
+// register right away, then settles to the plain running total a moment later - ready to
+// show " +1" on the next add.
+function bumpAllocationSummary(studentDict, delta) {
+ if (!allocationSummaryState || !studentDict) {
+ return;
+ }
+ var previousPositionsUsed = allocationSummaryState.positionsUsed;
+ var previousBreakHoursUsed = allocationSummaryState.breakHoursUsed;
+ var breakHoursDelta = applyAllocationDelta(studentDict, delta);
+
+ if (allocationFlashTimeoutId) {
+ clearTimeout(allocationFlashTimeoutId);
+ allocationFlashTimeoutId = null;
+ }
+
+ if (delta > 0) {
+ $("#allocationSummaryPositionsContracted").text(previousPositionsUsed + " +" + delta);
+ if (breakHoursDelta > 0) {
+ $("#allocationSummaryBreakHoursContracted").text(previousBreakHoursUsed + " +" + breakHoursDelta);
+ }
+ allocationFlashTimeoutId = setTimeout(renderAllocationSummary, 1500);
+ } else {
+ renderAllocationSummary();
+ }
+ checkLiveAllocationWarning();
+}
+
// TABLE LABELS
$("#contractHours").hide();
$("#hoursPerWeek").hide();
@@ -394,6 +585,7 @@ function deleteRow(glyphicon) {
for (var i = 0, row; row = table.rows[i]; i++) {
if (rowParent === table.rows[i]) {
$(glyphicon).parents("tr").remove();
+ bumpAllocationSummary(globalArrayOfStudents[i], -1);
globalArrayOfStudents.splice(i, 1);
if(globalArrayOfStudents.length > 1){
document.cookie = JSON.stringify(globalArrayOfStudents) + ";max-age=28800;";
@@ -571,6 +763,7 @@ function initialLSFInsert(studentDict){ //Add student info to the table if they
function createAndFillTable(studentDict) {
globalArrayOfStudents.push(studentDict);
document.cookie = JSON.stringify(globalArrayOfStudents) + ";max-age=28800;";
+ bumpAllocationSummary(studentDict, 1);
$("#mytable").show();
$("#jobTable").show();
$("#hoursTable").show();
diff --git a/app/templates/admin/allPendingForms.html b/app/templates/admin/allPendingForms.html
index 651cb2d1c..0de4c6f98 100644
--- a/app/templates/admin/allPendingForms.html
+++ b/app/templates/admin/allPendingForms.html
@@ -120,7 +120,7 @@
{{title}}
{% else %}
{# APPROVE #}
- {% if allForms.formID.studentSupervisee.isActive and allForms.formID.supervisor.isActive %} {%endif%}
+