Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b4d408f
Wire Current Allocation card to real Allocation/LaborStatusForm data …
DanielRukwasha Jul 7, 2026
0d1dad4
Remove Request Allocation button and relabel AY as Term on allocation…
DanielRukwasha Jul 7, 2026
747a75e
Add Primary/Secondary titles above the hour-band breakdown lists
DanielRukwasha Jul 7, 2026
391728a
Merge remote-tracking branch 'origin/allocation_card' into allocation…
DanielRukwasha Jul 8, 2026
0d64daf
Add allocation warning to pending LSF approval modal (#622)
DanielRukwasha Jul 8, 2026
ac0ace0
Highlight positions and break hours independently in allocation warning
DanielRukwasha Jul 8, 2026
ca34008
Flash a warning after approving forms if the department is now over-a…
DanielRukwasha Jul 9, 2026
035cda0
Fix over-allocation check to look at each hour-band, not just the total
DanielRukwasha Jul 10, 2026
f71b525
Merge remote-tracking branch 'origin/department-portal-base' into 622…
DanielRukwasha Jul 10, 2026
3ff622a
Add over-allocation warning on labor status form (#615)
DanielRukwasha Jul 13, 2026
b148007
Merge remote-tracking branch 'origin/development' into 615-over-alloc…
DanielRukwasha Jul 13, 2026
a6fde6c
Merge branch 'department-portal-base' into 615-over-allocation-warnin…
DanielRukwasha Jul 28, 2026
b9565b8
Merge branch 'department-portal-base' into 615-over-allocation-warnin…
DanielRukwasha Jul 30, 2026
e33755a
Address remaining PR review comments on the over-allocation warning f…
DanielRukwasha Jul 30, 2026
9069a1b
Always show individual approve checkbox regardless of student/supervi…
DanielRukwasha Aug 3, 2026
40b11be
removing x/y allocatioin to X remaining on the forms to make pending
DanielRukwasha Aug 3, 2026
3c7b838
real time small table display the current allocation sitution
DanielRukwasha Aug 3, 2026
56396ac
new real table update allocation situation on the labor status form
DanielRukwasha Aug 3, 2026
c10060e
Merge remote-tracking branch 'origin/department-portal-base' into 615…
DanielRukwasha Aug 3, 2026
a63e0e8
warn when staged students overallocate a department before submission
DanielRukwasha Aug 3, 2026
0b72402
Show live over-allocation warnings on the pending-forms list page
DanielRukwasha Aug 4, 2026
8e95894
Merge branch 'department-portal-base' into 615-over-allocation-warnin…
MImran2002 Aug 4, 2026
1ad8faf
Tie allocation summary/check to the selected term, not the server ope…
DanielRukwasha Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion app/controllers/main_routes/laborStatusForm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
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*
Comment thread
DanielRukwasha marked this conversation as resolved.
from app.logic.userInsertFunctions import*
from app.models.supervisor import Supervisor
from app.logic.tracy import Tracy
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


@main_bp.route('/laborstatusform', methods=['GET'])
Expand Down Expand Up @@ -170,6 +171,15 @@ def checkTotalHours(termCode, student, hours):
totalHours = totalHours + int(hours)
return json.dumps(totalHours)

@main_bp.route("/laborstatusform/checkallocation/<departmentOrg>/<departmentAcct>/<jobType>/<hours>", methods=["GET"])
Comment thread
DanielRukwasha marked this conversation as resolved.
Outdated
def checkAllocation(departmentOrg, departmentAcct, jobType, hours):
""" Checks the department's allocation status for the hour-band being submitted. """
dept = Department.get_or_none(Department.ORG == departmentOrg, Department.ACCOUNT == departmentAcct)
if not dept:
return jsonify(None)
Comment thread
DanielRukwasha marked this conversation as resolved.
Outdated
status = getBandAllocationStatus(dept, g.openTerm, jobType, int(hours))
return jsonify(status)

@main_bp.route("/laborStatusForm/modal/releaseAndRehire", methods=['POST'])
def releaseAndRehire():
try:
Expand Down
24 changes: 19 additions & 5 deletions app/controllers/main_routes/main_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@
from app.models.supervisor import Supervisor
from app.models.supervisorDepartment import SupervisorDepartment
from app.models.student import Student
from app.models.laborStatusForm import LaborStatusForm
from app.models.formHistory import FormHistory
from app.models.term import Term
from app.controllers.admin_routes.allPendingForms import checkAdjustment
from app.controllers.main_routes import main_bp
from app.logic.download import CSVMaker, saveFormSearchResult, retrieveFormSearchResult
from app.logic.search import getDepartmentsForSupervisor, searchPerson, searchSupervisorPortal
from app.login_manager import require_login, logout
from app.logic.getTableData import getDatatableData
from app.logic.banner import Banner
from app.logic.tracy import Tracy
from app.logic.allocation import getAllocationSummary
from app.models.positionHistory import PositionHistory
from app.logic.getPositions import getActivePositions


@main_bp.route('/logout', methods=['GET'])
def triggerLogout():
return redirect(logout())
Expand Down Expand Up @@ -65,13 +64,28 @@ def departmentPortal(org=None,account=None):
else:
departments = list(getDepartmentsForSupervisor(g.currentUser).order_by(Department.isActive.desc(), Department.DEPT_NAME.asc()))

staff = Tracy().getSupervisors()
supervisors = []

for i in staff:
if i.ORG == org:
supervisors.append(i.FIRST_NAME + " " + i.LAST_NAME + " (" + i.EMAIL + ")")

allocationSummary = getAllocationSummary(dept, g.openTerm)
positionsList, posURL = getActivePositions(dept)

return render_template('main/departmentPortal.html',
return render_template('main/departmentPortal.html',
departments = departments,
department = dept,
positions = positionsList,
posURL = posURL)
posURL = posURL,
supervisors = supervisors,
allocation = allocationSummary['allocation'],
allocationBands = allocationSummary['allocationBands'],
totalPositionsAllocated = allocationSummary['totalPositionsAllocated'],
totalPositionsUsed = allocationSummary['totalPositionsUsed'],
breakHoursUsed = allocationSummary['breakHoursUsed'],
currentTerm = g.openTerm)

@main_bp.route('/supervisorPortal/addUserToDept', methods=['GET', 'POST'])
def addUserToDept():
Expand Down
32 changes: 26 additions & 6 deletions app/logic/allPendingForms.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Comment thread
DanielRukwasha marked this conversation as resolved.
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):
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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):
Expand Down
123 changes: 123 additions & 0 deletions app/logic/allocation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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

# Each entry is (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 getAllocationSummary(dept, term):
Comment thread
DanielRukwasha marked this conversation as resolved.
Outdated
summary = {
'allocation': None,
'allocationBands': None,
'totalPositionsAllocated': None,
'totalPositionsUsed': None,
'breakHoursUsed': None,
}

if not (dept and term):
return summary

allocation = Allocation.get_or_none(Allocation.department == dept, Allocation.termCode == term)
summary['allocation'] = allocation
if not allocation:
return summary

allocationBands = {}
for fieldName, jobType, hours in ALLOCATION_BAND_FIELDS:
used = (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())
allocationBands[fieldName] = {'used': used, 'allocated': getattr(allocation, fieldName)}

summary['allocationBands'] = allocationBands
summary['totalPositionsAllocated'] = sum(band['allocated'] for band in allocationBands.values())
summary['totalPositionsUsed'] = sum(band['used'] for band in allocationBands.values())

# 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)]
summary['breakHoursUsed'] = (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 summary


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

summary = getAllocationSummary(dept, term)
if not summary['allocationBands']:
return None

band = summary['allocationBands'][fieldName]
return {
'label': BAND_LABELS[fieldName],
'used': band['used'],
'allocated': band['allocated'],
'remaining': band['allocated'] - band['used'],
'isOverAllocated': band['used'] > band['allocated'],
}


def getAllocationWarning(dept, term):
summary = getAllocationSummary(dept, term)
if not summary['allocation']:
return None

positionsRemaining = summary['totalPositionsAllocated'] - summary['totalPositionsUsed']
breakHoursRemaining = summary['allocation'].breakHours - summary['breakHoursUsed']

# 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': band['used'], 'allocated': band['allocated']}
for fieldName, band in summary['allocationBands'].items()
if band['used'] > band['allocated']
]
isPositionsOverAllocated = positionsRemaining < 0 or bool(overAllocatedBands)
isBreakHoursOverAllocated = breakHoursRemaining < 0

return {
'departmentName': dept.DEPT_NAME,
'totalPositionsAllocated': summary['totalPositionsAllocated'],
'totalPositionsUsed': summary['totalPositionsUsed'],
'positionsRemaining': positionsRemaining,
'isPositionsOverAllocated': isPositionsOverAllocated,
'overAllocatedBands': overAllocatedBands,
'breakHoursAllocated': summary['allocation'].breakHours,
'breakHoursUsed': summary['breakHoursUsed'],
'breakHoursRemaining': breakHoursRemaining,
'isBreakHoursOverAllocated': isBreakHoursOverAllocated,
'isOverAllocated': isPositionsOverAllocated or isBreakHoursOverAllocated,
}
1 change: 0 additions & 1 deletion app/models/positionHistory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ class PositionHistory(baseModel):

class Meta:
indexes = ( (('positionCode', 'revisionDate', 'status'), True), )

39 changes: 37 additions & 2 deletions app/static/js/allPendingForms.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,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);
}
}
});
Expand All @@ -112,6 +112,40 @@ 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.
function updateAllocationWarnings(allocationWarnings) {
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 positionsStyle = w.isPositionsOverAllocated ? overStyle : '';
var breakHoursStyle = w.isBreakHoursOverAllocated ? overStyle : '';
var positionsFlag = w.isPositionsOverAllocated ? ' &#9888; Over allocation' : '';
var breakHoursFlag = w.isBreakHoursOverAllocated ? ' &#9888; Over 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 = '<br><span style="' + overStyle + '">Over on: ' + bandStrings.join(', ') + '</span>';
}
var html = '<div class="alert ' + boxClass + '" role="alert">' +
'<strong>' + w.departmentName + ' Allocation</strong><br>' +
'<span style="' + positionsStyle + '">Total Positions (all bands): ' + w.totalPositionsUsed + ' / ' + w.totalPositionsAllocated +
Comment thread
DanielRukwasha marked this conversation as resolved.
Outdated
' allocated (' + w.positionsRemaining + ' remaining)' + positionsFlag + '</span>' + bandDetail + '<br>' +
'<span style="' + breakHoursStyle + '">Break Hours: ' + w.breakHoursUsed + ' / ' + w.breakHoursAllocated +
' allocated (' + w.breakHoursRemaining + ' remaining)' + breakHoursFlag + '</span>' +
'</div>';
$('#allocationWarnings').append(html);
}
}


$('#approvalModal').on('hidden.bs.modal', function () {// Makes the close functionality work when clicking outside of the modal
approvalModalClose();
Expand All @@ -120,6 +154,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.
}

Expand Down
Loading