Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
38 changes: 37 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, getAllocationWarning


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

@main_bp.route("/laborstatusform/checkallocation", methods=["GET"])
Comment thread
DanielRukwasha marked this conversation as resolved.
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")
Comment thread
DanielRukwasha marked this conversation as resolved.

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:
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
139 changes: 139 additions & 0 deletions app/logic/allocation.py
Original file line number Diff line number Diff line change
@@ -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,
}
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), )

6 changes: 3 additions & 3 deletions app/static/css/laborStatusForm.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -68,7 +68,7 @@ selectpicker, label {
}

#plus {
text-align: center;
text-align: left;
padding-bottom: 30px;
}

Expand Down
Loading