diff --git a/app/config/contributors.yaml b/app/config/contributors.yaml index 2a47673ca..935f4bcbc 100755 --- a/app/config/contributors.yaml +++ b/app/config/contributors.yaml @@ -62,4 +62,6 @@ contributors: - name: "Bhushan Sah " username: "sahb" year: 2029 - \ No newline at end of file + - name: "Bright Feitsop" + username: "feitsopb" + year: 2029 \ No newline at end of file diff --git a/app/controllers/admin_routes/manageDepartments.py b/app/controllers/admin_routes/manageDepartments.py index 238eb2859..8ed4a69f6 100644 --- a/app/controllers/admin_routes/manageDepartments.py +++ b/app/controllers/admin_routes/manageDepartments.py @@ -15,14 +15,16 @@ from app.models.allocation import * from app.models.laborStatusForm import * -from app.logic.manageDepartments import * +from app.logic.manageDepartments import * +from app.logic.emailHandler import emailHandler from app.logic.allocationManager import allocationExists from app.logic.academicYearManager import getCurrentAndNextAY @admin.route('/admin/manageDepartments/', methods=['GET']) -def manageDepartments(): +@admin.route('/admin/manageDepartments/', methods=['GET']) +def manageDepartments(academicYear = None): """ Returns the Manage Departments page, which allows the admin to view all the departments and their allocations. @@ -38,8 +40,13 @@ def manageDepartments(): elif currentUser.supervisor: return render_template('errors/403.html'), 403 - currentAY, nextAY = getCurrentAndNextAY() - chosenAY = Term.get(Term.termCode == currentAY.termCode) + + # The condition below may be deleted if the routing to the Manage Departments page is changed. + academicYear = g.currentAY[0] * 100 if academicYear is None else int(academicYear) + + + currentAY, nextAY = generateAdjacentYears(academicYear) + chosenAY = Term.get(Term.termCode == academicYear) breakHoursByDepartment = {row["department"]: str(row["totalHours"] or 0) for row in getUsedBreakHours(chosenAY)} @@ -59,6 +66,7 @@ def manageDepartments(): allSupervisors = allSupervisors, currentAY = currentAY, nextAY = nextAY, + chosenAY = chosenAY, academicYear = chosenAY.termName, breakHoursByDepartment = breakHoursByDepartment, allocationStatus = allocationStatus @@ -85,6 +93,36 @@ def complianceStatusCheck(): +@admin.route('/admin/manageDepartments/annualPositionReview', methods=['POST']) +def annualPositionReviewRequest(): + """ + Sends an Annual Position Review request email to every active department's + Labor Coordinators and supervisors for the selected academic year, and + records the request. Triggered from the Manage Departments page. + """ + currentUser = require_login() + if not currentUser or not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent): + return jsonify({"Success": False}), 403 + + rsp = request.get_json(silent=True) + + try: + academicYear = int(rsp["academicYear"]) + except (TypeError, ValueError, KeyError): + return jsonify({"Success": False, "message": "Request must include a valid academicYear."}), 400 + + try: + handler = emailHandler(academicYearTermCode=academicYear) + result = handler.sendAnnualPositionReviewRequests(currentUser) + if result["failedDepartments"]: + result["message"] = "Requests sent to {} of {} departments. Failed departments: {}.".format( + result["sentCount"], result["departmentCount"], ", ".join(result["failedDepartments"]) + ) + return jsonify({"Success": False, **result}) + return jsonify({"Success": True, **result}) + except Exception: + return jsonify({"Success": False}) + @admin.route('/admin/manageDepartments///allocationReview', methods=['GET']) def allocationReview(org=None, account=None): """ @@ -116,12 +154,12 @@ def allocationReview(org=None, account=None): # checking if the allocation has already been approved - if allocationExists(nextAY.termCode, dept, isFinal=True): + if allocationExists(nextAY.termCode, dept, isFinal=True): flash("You cannot reapprove an allocation request.", "info") return redirect('/admin/manageDepartments/') - - # checking if the department has requested any allocation review + + # checking if the department has requested any allocation review if not allocationExists(nextAY.termCode, dept, isFinal=False): flash(f"The {dept.DEPT_NAME} department has not requested an allocation review yet.", "info") return redirect('/admin/manageDepartments/') @@ -133,7 +171,7 @@ def allocationReview(org=None, account=None): return render_template('admin/allocationReview.html', - department = dept, + department = dept, nextAY = nextAY, currentAlloc = currentAlloc, requestedAlloc = requestedAlloc @@ -143,7 +181,7 @@ def allocationReview(org=None, account=None): @admin.route('/admin/allocationReview/approve', methods=['POST']) def approveAllocationReview(): - + # Retrieving the current and following academic years currentAY, nextAY = getCurrentAndNextAY() @@ -154,8 +192,8 @@ def approveAllocationReview(): requester = request.form.get("requester", type=int, default=None) # saving the newly approved allocation - newApprovedAlloc = Allocation.create(termCode = nextAY.termCode, - department = requester, + newApprovedAlloc = Allocation.create(termCode = nextAY.termCode, + department = requester, isFinal = True, approvedBy = approverID, approvedOn = date.today(), diff --git a/app/logic/emailHandler.py b/app/logic/emailHandler.py index 1de8ef492..ee3a880fa 100644 --- a/app/logic/emailHandler.py +++ b/app/logic/emailHandler.py @@ -16,82 +16,96 @@ from app import app import os from datetime import datetime, date +from app.models.department import Department +from app.models.term import Term +from app.models.positionHistory import PositionHistory +from app.logic.getSupervisors import getSupervisors class emailHandler(): - def __init__(self, formHistoryKey): + def __init__(self, formHistoryKey=None, academicYearTermCode=None): self.mail = Mail(app) - self.formHistory = FormHistory.get(FormHistory.formHistoryID == formHistoryKey) - self.laborStatusForm = self.formHistory.formID - self.term = self.laborStatusForm.termCode - self.student = self.laborStatusForm.studentSupervisee - self.studentEmail = self.student.STU_EMAIL - self.creatorEmail = self.formHistory.createdBy.email - self.supervisorEmail = self.laborStatusForm.supervisor.EMAIL - self.date = self.laborStatusForm.startDate.strftime("%m/%d/%Y") - self.weeklyHours = str(self.laborStatusForm.weeklyHours) - self.contractHours = str(self.laborStatusForm.contractHours) - self.adminName = "" - self.positions = LaborStatusForm.select().where(LaborStatusForm.termCode == self.term, LaborStatusForm.studentSupervisee == self.student) - self.supervisors = [] - for position in self.positions: - self.supervisors.append(position.supervisor) - - if not self.term.isBreak: - try: - ayTermCode = str(self.laborStatusForm.termCode.termCode)[:-2] + '00' - self.primaryEmail = None - self.primaryForm = None - self.primaryForm = FormHistory.select().join_from(FormHistory, LaborStatusForm) \ - .join_from(FormHistory, HistoryType).join_from(FormHistory, Status) \ - .where((FormHistory.formID.jobType == "Primary") & - (FormHistory.formID.studentSupervisee == self.laborStatusForm.studentSupervisee) & - ((FormHistory.formID.termCode == self.laborStatusForm.termCode) | (FormHistory.formID.termCode == ayTermCode)) & - (FormHistory.historyType.historyTypeName == "Labor Status Form") & - ~(FormHistory.status.statusName % "Denied%")).get() - self.primaryEmail = self.primaryForm.formID.supervisor.EMAIL - except DoesNotExist: - # This case happens from some of the old data - pass - - self.link = "" - self.releaseReason = "" - self.releaseDate = "" - self.newAdjustmentField = "" - self.oldAdjustmentField = "" - - # generating a confirmation link for student approval - self.confirmationLink = "" - if self.laborStatusForm.confirmationToken: - self.confirmationLink = f"{request.host_url}studentResponse/confirm?token={self.laborStatusForm.confirmationToken}" - - - if self.formHistory.adjustedForm: - if self.formHistory.adjustedForm.fieldAdjusted == "supervisor": - from app.logic.userInsertFunctions import createSupervisorFromTracy - newSupervisor = createSupervisorFromTracy(bnumber=self.formHistory.adjustedForm.newValue) - self.newAdjustmentField = "Pending new Supervisor: {0} {1}".format(newSupervisor.FIRST_NAME, newSupervisor.LAST_NAME) - self.oldAdjustmentField = "Current Supervisor: {0} {1}".format(self.formHistory.formID.supervisor.FIRST_NAME, self.formHistory.formID.supervisor.LAST_NAME) - elif self.formHistory.adjustedForm.fieldAdjusted == "position": - currentPosition = Tracy().getPositionFromCode(self.formHistory.adjustedForm.oldValue) - newPosition = Tracy().getPositionFromCode(self.formHistory.adjustedForm.newValue) - self.oldAdjustmentField = "Current Position: {0} ({1})".format(currentPosition.POSN_TITLE, currentPosition.WLS) - self.newAdjustmentField = "Pending new Position: {0} ({1})".format(newPosition.POSN_TITLE, newPosition.WLS) - else: - self.oldAdjustmentField = "Current Hours: {0}".format(self.formHistory.adjustedForm.oldValue) - self.newAdjustmentField = "Pending new Hours: {0}".format(self.formHistory.adjustedForm.newValue) - - try: - self.releaseDate = self.formHistory.releaseForm.releaseDate.strftime("%m/%d/%Y") - self.releaseReason = self.formHistory.releaseForm.reasonForRelease + # emailHandler was originally built entirely around a single + # LaborStatusForm (formHistoryKey). Annual Position Review isn't tied + # to a form at all - it's scoped to an academic year across every + # department - so construction branches on whichever was given. + if formHistoryKey is not None: + self.formHistory = FormHistory.get(FormHistory.formHistoryID == formHistoryKey) + self.laborStatusForm = self.formHistory.formID + self.term = self.laborStatusForm.termCode + self.student = self.laborStatusForm.studentSupervisee + self.studentEmail = self.student.STU_EMAIL + self.creatorEmail = self.formHistory.createdBy.email + self.supervisorEmail = self.laborStatusForm.supervisor.EMAIL + self.date = self.laborStatusForm.startDate.strftime("%m/%d/%Y") + self.weeklyHours = str(self.laborStatusForm.weeklyHours) + self.contractHours = str(self.laborStatusForm.contractHours) + self.adminName = "" + self.positions = LaborStatusForm.select().where(LaborStatusForm.termCode == self.term, LaborStatusForm.studentSupervisee == self.student) + self.supervisors = [] + for position in self.positions: + self.supervisors.append(position.supervisor) + + if not self.term.isBreak: + try: + ayTermCode = str(self.laborStatusForm.termCode.termCode)[:-2] + '00' + self.primaryEmail = None + self.primaryForm = None + self.primaryForm = FormHistory.select().join_from(FormHistory, LaborStatusForm) \ + .join_from(FormHistory, HistoryType).join_from(FormHistory, Status) \ + .where((FormHistory.formID.jobType == "Primary") & + (FormHistory.formID.studentSupervisee == self.laborStatusForm.studentSupervisee) & + ((FormHistory.formID.termCode == self.laborStatusForm.termCode) | (FormHistory.formID.termCode == ayTermCode)) & + (FormHistory.historyType.historyTypeName == "Labor Status Form") & + ~(FormHistory.status.statusName % "Denied%")).get() + self.primaryEmail = self.primaryForm.formID.supervisor.EMAIL + except DoesNotExist: + # This case happens from some of the old data + pass + + self.link = "" + self.releaseReason = "" + self.releaseDate = "" + self.newAdjustmentField = "" + self.oldAdjustmentField = "" + + # generating a confirmation link for student approval + self.confirmationLink = "" + if self.laborStatusForm.confirmationToken: + self.confirmationLink = f"{request.host_url}studentResponse/confirm?token={self.laborStatusForm.confirmationToken}" + + + if self.formHistory.adjustedForm: + if self.formHistory.adjustedForm.fieldAdjusted == "supervisor": + from app.logic.userInsertFunctions import createSupervisorFromTracy + newSupervisor = createSupervisorFromTracy(bnumber=self.formHistory.adjustedForm.newValue) + self.newAdjustmentField = "Pending new Supervisor: {0} {1}".format(newSupervisor.FIRST_NAME, newSupervisor.LAST_NAME) + self.oldAdjustmentField = "Current Supervisor: {0} {1}".format(self.formHistory.formID.supervisor.FIRST_NAME, self.formHistory.formID.supervisor.LAST_NAME) + elif self.formHistory.adjustedForm.fieldAdjusted == "position": + currentPosition = Tracy().getPositionFromCode(self.formHistory.adjustedForm.oldValue) + newPosition = Tracy().getPositionFromCode(self.formHistory.adjustedForm.newValue) + self.oldAdjustmentField = "Current Position: {0} ({1})".format(currentPosition.POSN_TITLE, currentPosition.WLS) + self.newAdjustmentField = "Pending new Position: {0} ({1})".format(newPosition.POSN_TITLE, newPosition.WLS) + else: + self.oldAdjustmentField = "Current Hours: {0}".format(self.formHistory.adjustedForm.oldValue) + self.newAdjustmentField = "Pending new Hours: {0}".format(self.formHistory.adjustedForm.newValue) - except Exception as e: - # The error you should get when the form is not a release form - # is the 'AttributeError' error. We expect to get the 'AttributeError', - # but if we get anything else then we want to print the error - if e.__class__.__name__ != "AttributeError": - print (e) + try: + self.releaseDate = self.formHistory.releaseForm.releaseDate.strftime("%m/%d/%Y") + self.releaseReason = self.formHistory.releaseForm.reasonForRelease + + except Exception as e: + # The error you should get when the form is not a release form + # is the 'AttributeError' error. We expect to get the 'AttributeError', + # but if we get anything else then we want to print the error + if e.__class__.__name__ != "AttributeError": + print (e) + + elif academicYearTermCode is not None: + self.term = Term.get(Term.termCode == academicYearTermCode) + else: + raise ValueError("emailHandler requires either formHistoryKey or academicYearTermCode") def send(self, message: Message): if app.config['ENV'] == 'production' or app.config['ALWAYS_SEND_MAIL']: @@ -110,7 +124,67 @@ def send(self, message: Message): else: print("ENV: {}. Email not sent to {}, subject '{}'.".format(app.config['ENV'], message.recipients, message.subject)) + def sendAnnualPositionReviewRequests(self, requestingUser): + """ + Sends an Annual Position Review request email to every active department's + Labor Coordinators and supervisors, and records that the request was made + for this handler's academic year (self.term). + """ + template = EmailTemplate.get_or_none(EmailTemplate.purpose == "Annual Position Review Request") + if template is None: + raise ValueError("The 'Annual Position Review Request' email template is missing.") + + departments = Department.select().where(Department.isActive == True) + + sentCount = 0 + failedDepartments = [] + for department in departments: + # A review is considered "requested" for every active department as soon + # as this runs, whether or not there's currently anyone to email - a + # department with no supervisors/coordinators assigned is itself worth + # surfacing, not silently skipping. + existingReview = PositionHistory.get_or_none( + PositionHistory.academicYear == self.term, + PositionHistory.department == department, + PositionHistory.positionCode.is_null(True) + ) + if existingReview: + existingReview.requestedOn = datetime.now() + existingReview.requestedBy = requestingUser + existingReview.save() + else: + PositionHistory.create( + academicYear=self.term, + department=department, + requestedOn=datetime.now(), + requestedBy=requestingUser + ) + + supervisors, laborCoordinators = getSupervisors(department) + recipients = {person["email"] for person in supervisors + laborCoordinators if person["email"]} + if not recipients: + continue + subject = template.subject.replace("@@AcademicYear@@", self.term.termName) + body = template.body.replace("@@Department@@", department.DEPT_NAME).replace("@@AcademicYear@@", self.term.termName) + + try: + message = Message(subject, recipients=list(recipients)) + message.html = body + self.send(message) + except Exception as error: + failedDepartments.append(department.DEPT_NAME) + print("Failed to send Annual Position Review request for department {}: {}".format(department.DEPT_NAME, error)) + continue + + sentCount += 1 + print("Sent Annual Position Review request to {} for department {}.".format(", ".join(recipients), department.DEPT_NAME)) + print("{} Annual Position Review requests sent for academic year {}.".format(sentCount, self.term.termName)) + return { + "sentCount": sentCount, + "departmentCount": departments.count(), + "failedDepartments": failedDepartments + } # The methods of this class each handle a different email situation. Some of the methods need to handle # "primary" and "secondary" forms differently, but a majority do not need to differentiate between the two. diff --git a/app/logic/manageDepartments.py b/app/logic/manageDepartments.py index 9f823c784..31daf6477 100644 --- a/app/logic/manageDepartments.py +++ b/app/logic/manageDepartments.py @@ -14,6 +14,32 @@ +def generateAdjacentYears(academicYearTermCode=None): + """ + Generates the current and the following academic years. + """ + + currentYear, nextYear = g.currentAY + + currentAYCode = currentYear * 100 + nextAYCode = nextYear * 100 + + # Admins can only view the current positions and the requested positions for the incoming academic year + if academicYearTermCode not in (None, currentAYCode, nextAYCode): + abort(400) + + + currentAY, _ = Term.get_or_create( + termCode=currentAYCode, + defaults={"termName": "AY {}-{}".format(currentYear, currentYear + 1), "isAcademicYear": True} + ) + + nextAY, _ = Term.get_or_create( + termCode=nextAYCode, + defaults={"termName": "AY {}-{}".format(nextYear, nextYear + 1), "isAcademicYear": True} + ) + + return (currentAY, nextAY) def getUsedBreakHours(term): """ Returns the total number of break hours used by each department for a given term. diff --git a/app/models/positionHistory.py b/app/models/positionHistory.py index fd38f26c3..ab3a3ac27 100644 --- a/app/models/positionHistory.py +++ b/app/models/positionHistory.py @@ -1,14 +1,20 @@ from app.models import * from app.models.department import Department +from app.models.term import Term +from app.models.user import User class PositionHistory(baseModel): - positionTitle = CharField() - positionCode = CharField() + positionTitle = CharField(null=True) + positionCode = CharField(null=True) department = ForeignKeyField(Department) - status = CharField() # Active, Inactive, Requested - wls = IntegerField() - revisionDate = DateField() - revisedBy = CharField() + status = CharField(null=True) + wls = IntegerField(null=True) + revisionDate = DateField(null=True) + revisedBy = CharField(null=True) + description = TextField(default=None) + academicYear = ForeignKeyField(Term, null=True) + requestedOn = DateTimeField(null=True) + requestedBy = ForeignKeyField(User, null=True) class Meta: indexes = ( (('positionCode', 'revisionDate', 'status'), True), ) diff --git a/app/models/positionReview.py b/app/models/positionReview.py new file mode 100644 index 000000000..fa11d3ce4 --- /dev/null +++ b/app/models/positionReview.py @@ -0,0 +1,14 @@ +from app.models import * +from app.models.department import Department +from app.models.term import Term +from app.models.user import User + + +class PositionReview(baseModel): + academicYear = ForeignKeyField(Term) + department = ForeignKeyField(Department) + requestedOn = DateTimeField() + requestedBy = ForeignKeyField(User) + + class Meta: + indexes = ( (('academicYear', 'department'), True), ) diff --git a/app/static/js/annualPositionReview.js b/app/static/js/annualPositionReview.js new file mode 100644 index 000000000..929609530 --- /dev/null +++ b/app/static/js/annualPositionReview.js @@ -0,0 +1,47 @@ +function submitAnnualPositionReview(button) { +/* + POSTs the Annual Position Review request for the currently selected academic year. + Sends a review request email to every active department's Labor Coordinators and + supervisors, then shows a success/failure flash message. + + RETURNS: None +*/ + var academicYear = $('[data-target="#annualPositionModal"]').data('academic-year'); + + // Disable immediately so a double-click can't fire this request twice - + // PositionHistory dedupes the request record, but the emails would still go out + // more than once. Re-enabled in complete regardless of outcome so a retry + // after a failure is possible without reloading the page. + $(button).prop("disabled", true); + + $.ajax({ + method: "POST", + url: "/admin/manageDepartments/annualPositionReview", + dataType: "json", + contentType: "application/json", + data: JSON.stringify({"academicYear": academicYear}), + success: function(response) { + $("#annualPositionModal").modal("hide"); + + if (response["Success"]) { + msgFlash("Position review requests sent to " + response["sentCount"] + " of " + response["departmentCount"] + " departments.", "success"); + } else { + var msg = response["message"] || ("Requests sent to " + response["sentCount"] + " of " + response["departmentCount"] + " departments, but some requests failed."); + msgFlash(msg, "fail"); + } + }, + error: function(jqXHR) { + // Covers cases success: never sees - a 403 (not a labor admin), a 500, + // or the request failing outright. Leaves the modal open so the admin + // can retry instead of silently doing nothing. + var payload = jqXHR.responseJSON || {}; + var msg = jqXHR.status === 403 + ? "You don't have permission to send Annual Position Review requests." + : (payload["message"] || "Something went wrong sending the Annual Position Review requests."); + msgFlash(msg, "fail"); + }, + complete: function() { + $(button).prop("disabled", false); + } + }) +} diff --git a/app/static/js/emailTemplates.js b/app/static/js/emailTemplates.js index 6ef0f715c..f7aba7f02 100644 --- a/app/static/js/emailTemplates.js +++ b/app/static/js/emailTemplates.js @@ -13,10 +13,61 @@ function getEmailArray() { dataType: "json", success: function(response) { emailTemplateArray = response; + prefillFromQueryParams(); } }) } +function prefillFromQueryParams() { + // Allows deep-linking into this page (e.g. from another admin page's + // "Edit Email Template" button) with the Recipient/Form Type/Action + // selectpickers pre-selected, so the template loads without the admin + // having to click through the cascading dropdowns manually. + var params = new URLSearchParams(window.location.search); + var recipient = params.get("audience"); + var formType = params.get("formType"); + var action = params.get("action"); + if (!recipient) { + return; + } + + // populatePurpose() ends by inserting the body into the CKEditor instance, + // which initializes asynchronously (CKEDITOR.replace() in emailTemplates.html). + // Running this cascade immediately on page load (unlike a human clicking + // through the dropdowns) can race ahead of that, so wait for the editor + // to be ready before touching it. + function runPrefill() { + $("#recipient").val(recipient).selectpicker("refresh"); + populateFormType(); + + if (formType) { + $("#formType").val(formType).selectpicker("refresh"); + populateAction(); + + if (action) { + $("#action").val(action).selectpicker("refresh"); + populatePurpose(); + } + } + } + + // CKEDITOR.instances["editor1"] is registered as soon as CKEDITOR.replace() + // is called, well before the editor is actually ready to accept content + // (that's the "ready" status). Checking mere existence isn't enough here. + var editor = CKEDITOR.instances["editor1"]; + if (editor && editor.status === "ready") { + runPrefill(); + } else if (editor) { + editor.on("instanceReady", runPrefill); + } else { + CKEDITOR.on("instanceReady", function(evt) { + if (evt.editor.name === "editor1") { + runPrefill(); + } + }); + } +} + function populateFormType() { // populates Form Type only when Recipient is selected $("#formType").prop("disabled", false); diff --git a/app/templates/admin/manageDepartments.html b/app/templates/admin/manageDepartments.html index 88fcf297e..d3988de88 100755 --- a/app/templates/admin/manageDepartments.html +++ b/app/templates/admin/manageDepartments.html @@ -7,6 +7,8 @@ src="{{url_for('static', filename='js/manageDepartments.js') }}?u={{lastStaticUpdate}}"> + {% endblock %} {% block app_content %}
Click to Skip @@ -45,26 +47,23 @@

Manage Departments


-
+
{% include "snips/uploadAllocations.html" %} -
- -
+ +
{% include "snips/annualAllocationReview.html" %} - {% include "snips/annualPositionReview.html" %} -
@@ -86,15 +85,17 @@

Manage Departments


+ +
- - - + + + @@ -127,31 +128,20 @@

Manage Departments

Secondary: {{department.lsfCountSecondaries}} of {{department.totalSecondaries}} -
- - Break: {{ breakHoursByDepartment.get(department.departmentID, 0) }} of {{ - department.allocation.breakHours }} hours - - + +
DepartmentStatusCurrent Allocations
({{ academicYear }})
Requested Allocations
({{ nextAY.termName }})
PositionsAllocation Status
({{ academicYear }})
Break Hours
(Used/Given)
Actions
- - - Primary: - {{department.lsfCountPrimaries}} of {{department.totalPrimaries}} - -
- - Secondary: - {{department.lsfCountSecondaries}} of {{department.totalSecondaries}} - -
- - Break: {{ breakHoursByDepartment.get(department.departmentID, 0) }} of {{ department.allocation.breakHours }} hours - +
+ {{ breakHoursByDepartment.get(department.departmentID, 0) }} / {{ department.allocation.breakHours }} + @@ -162,12 +152,9 @@

Manage Departments

diff --git a/app/templates/snips/annualPositionReview.html b/app/templates/snips/annualPositionReview.html index dac86a31e..547f75953 100644 --- a/app/templates/snips/annualPositionReview.html +++ b/app/templates/snips/annualPositionReview.html @@ -5,7 +5,7 @@ @@ -13,9 +13,9 @@