diff --git a/app/config/contributors.yaml b/app/config/contributors.yaml index 2a47673ca..d26cdd366 100755 --- a/app/config/contributors.yaml +++ b/app/config/contributors.yaml @@ -59,7 +59,4 @@ contributors: - name: "Oluwagbayi Makinde" username: "makindeo" year: 2024 - - name: "Bhushan Sah " - username: "sahb" - year: 2029 \ No newline at end of file diff --git a/app/controllers/main_routes/__init__.py b/app/controllers/main_routes/__init__.py index 54b32d3c5..da3323908 100755 --- a/app/controllers/main_routes/__init__.py +++ b/app/controllers/main_routes/__init__.py @@ -14,6 +14,7 @@ def injectGlobalData(): 'lastStaticUpdate': lastStaticUpdate} from app.controllers.main_routes import main_routes +from app.controllers.main_routes import departmentPortal from app.controllers.main_routes import laborStatusForm from app.controllers.main_routes import laborHistory from app.controllers.main_routes import alterLSF diff --git a/app/controllers/main_routes/departmentPortal.py b/app/controllers/main_routes/departmentPortal.py new file mode 100644 index 000000000..339f331b4 --- /dev/null +++ b/app/controllers/main_routes/departmentPortal.py @@ -0,0 +1,172 @@ +import re +from datetime import date +from flask import render_template, request, json, redirect, session, url_for, send_file, g, flash, jsonify +from peewee import JOIN, DoesNotExist, fn, Case +from functools import reduce +import operator +from app.logic.userInsertFunctions import createSupervisorFromTracy +from app.models.department import Department +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.laborReleaseForm import LaborReleaseForm +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 flask import abort +from app.logic.manageMembers import supervisorsDbToDict, currentAcademicYear +from app.logic.search import limitSearchByUserDepartment, studentDbToDict, usernameFromEmail +from app.logic.manageMembers import getCurrentDepartment,getDepartmentMembers,getReleasedFormIds,getStudentCounts,attachPositionCounts + +@main_bp.route('/department///members', methods=['GET']) +def manageMembers(org=None, account=None): + """Generates the Manage Members page.""" + currentUser = require_login() + if not currentUser or not currentUser.supervisor: + if currentUser.student: + return redirect(url_for('main.laborhistory', id=currentUser.student.ID)) + return render_template('errors/403.html'), 403 + + currentSupervisor = Supervisor.get(Supervisor.ID == currentUser.supervisor) + dept = getCurrentDepartment(org, account) + + members = getDepartmentMembers(dept) + counts = getStudentCounts(dept) + members = attachPositionCounts(members, counts) + + return render_template( + 'main/manageMembers.html', + members=members, + department=dept, + currentSupervisor=currentSupervisor, + currentAcademicYear=currentAcademicYear() + ) + + + +@main_bp.route('/members/search/', methods=['GET']) +def searchMember(query=None): + """ + Search student table and STUDATA for student results. + """ + currentUser = require_login() + accessAllowed = currentUser and (currentUser.supervisor or currentUser.isLaborAdmin) + if not accessAllowed: + return render_template('errors/403.html'), 403 + + recordedSupervisors = [] # supervisors recorded in the database + query = query.strip() + + displayedSupervisors = Supervisor.select() + + # bnumber search + if re.match(r'[Bb]\d+', query): + recordedSupervisors = list(map(supervisorsDbToDict, displayedSupervisors.where(Supervisor.ID % "{}%".format(query.upper())))) + + + # name search + else: + if " " not in query: + search = query.upper() + "%" + results = displayedSupervisors.where(Supervisor.preferred_name ** search | Supervisor.legal_name ** search | Supervisor.LAST_NAME ** search) + else: + search = query.upper().split() + firstQuery = search[0] + "%" + lastQuery = search[-1] + "%" + results = displayedSupervisors.where((Supervisor.preferred_name ** firstQuery | Supervisor.legal_name ** firstQuery) & Supervisor.LAST_NAME ** lastQuery) + + recordedSupervisors = list(map(supervisorsDbToDict, results)) + + + # combine lists, remove duplicates, and then sort + supervisors = list({v['bnumber']:v for v in (recordedSupervisors)}.values()) + supervisors = sorted(supervisors, key=lambda f: f['firstName'] + f['lastName']) + + return jsonify(supervisors) + +@main_bp.route('/members/coordinator_switch', methods=['POST']) +def coordinatorSwitch(): + """ + Assigns or unassignes a supervisor as a Labor Coordinator. + """ + data = request.get_json() + supervisorID = data.get("supervisorID") + isCoordinator = data.get("isCoordinator") + + departmentID = session.get('current_department_id') + if not departmentID: + return "", 400 + + member = SupervisorDepartment.get( + (SupervisorDepartment.supervisor == supervisorID) & + (SupervisorDepartment.department == departmentID) + ) + member.isCoordinator = isCoordinator + member.save() + + return "", 200 + + +@main_bp.route('/members/ban_switch', methods=['POST']) +def elegibilitySwitch(): + """ + Updates a supervisor's eligibility status. + """ + data = request.get_json() + supervisorID = data.get("supervisorID") + + member = Supervisor.get(Supervisor.ID == supervisorID) + + member.isBanned = not member.isBanned + member.save() + + return "", 200 + + +@main_bp.route('/members/remove', methods=['DELETE']) +def removeMember(): + """ + Removes a staff member from a department. + """ + data = request.get_json() + supervisorID = data.get("supervisorID") + + member = SupervisorDepartment.get( + (SupervisorDepartment.supervisor == supervisorID) & + (SupervisorDepartment.department == session['current_department_id']) + ) + member.delete_instance() + + return "", 200 + + + +@main_bp.route('/members/add', methods=['GET', 'POST']) +def addUserToDept(): + """ + Adds a user to a department. + """ + userDeptData = request.form + supervisorDeptRecord = SupervisorDepartment.get_or_none(supervisor = userDeptData['supervisorID'], department = userDeptData['departmentID']) + try: + if supervisorDeptRecord: + return "False" + + else: + supervisorID = userDeptData['supervisorID'] + if not Supervisor.get_or_none(Supervisor.ID == supervisorID): + createSupervisorFromTracy(bnumber=supervisorID) + + SupervisorDepartment.create(supervisor=supervisorID, department=userDeptData['departmentID']) + return "True" + + except Exception as e: + print(f'Could not add user to department: {e}') + return "", 500 \ No newline at end of file diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index b3af06c9e..5aa8841d1 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -1,5 +1,5 @@ from flask import render_template, request, json, redirect, url_for, send_file, g, flash, jsonify -from peewee import JOIN +from peewee import JOIN, DoesNotExist from functools import reduce import operator from app.models.department import Department @@ -16,6 +16,10 @@ 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.userInsertFunctions import createSupervisorFromTracy +from app.models.positionHistory import PositionHistory + @main_bp.route('/logout', methods=['GET']) def triggerLogout(): @@ -47,21 +51,50 @@ def supervisorPortal(): currentUser = currentUser ) -@main_bp.route('/supervisorPortal/addUserToDept', methods=['GET', 'POST']) -def addUserToDept(): - userDeptData = request.form - supervisorDeptRecord = SupervisorDepartment.get_or_none(supervisor = userDeptData['supervisorID'], department = userDeptData['departmentID']) +@main_bp.route('/department', methods=['GET']) +@main_bp.route('/department/', methods=['GET']) +@main_bp.route('/department//', methods=['GET']) +def departmentPortal(org=None,account=None): + if org and account: + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except (NameError, DoesNotExist): + dept = None + else: + dept = None + + + + + if g.currentUser.isLaborAdmin: + departments = list(Department.select().order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) + else: + departments = list(getDepartmentsForSupervisor(g.currentUser).order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) + + return render_template('main/departmentPortal.html', + departments = departments, + department = dept, + # supervisors = supervisor, + # laborCoordinators=laborCoordinators, + currentUser=g.currentUser, + # positions = positionsList, + # posUrl = posUrl + ) + +@main_bp.route('/department///positions', methods=['GET']) +def managePositions(org, account): try: - if supervisorDeptRecord: - return "False" + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except DoesNotExist: + return render_template('errors/404.html'), 404 - else: - SupervisorDepartment.create(supervisor=userDeptData['supervisorID'], department=userDeptData['departmentID']) - return "True" - - except Exception as e: - print(f'Could not add user to department: {e}') - return "", 500 + positions = Tracy().getPositionsFromDepartment(org, account) + print(positions) + return render_template('main/managepositions.html', + department = dept, + department_name = dept.DEPT_NAME, + positions = positions + ) @main_bp.route('/supervisorPortal/download', methods=['POST']) def downloadSupervisorPortalResults(): @@ -113,4 +146,4 @@ def submitToBanner(formHistoryId): if save_form_status: return "Form successfully submitted to Banner.", 200 else: - return "Submitting to Banner failed.", 500 \ No newline at end of file + return "Submitting to Banner failed.", 500 diff --git a/app/logic/manageMembers.py b/app/logic/manageMembers.py new file mode 100644 index 000000000..ab06f0029 --- /dev/null +++ b/app/logic/manageMembers.py @@ -0,0 +1,128 @@ +import re +from datetime import date +from flask import render_template, request, json, redirect, session, url_for, send_file, g, flash, jsonify +from peewee import JOIN, DoesNotExist, fn, Case +from functools import reduce +import operator +from app.logic.userInsertFunctions import createSupervisorFromTracy +from app.models.department import Department +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.laborReleaseForm import LaborReleaseForm +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 flask import abort +from app.logic.search import limitSearchByUserDepartment, studentDbToDict, usernameFromEmail + +def supervisorsDbToDict(supervisor): + """ + Given a supervisor object it will return a mapped Dict with supervisor data. + """ + dbToDict = {'username': usernameFromEmail(supervisor.EMAIL.strip()), + 'firstName': supervisor.FIRST_NAME.strip(), + 'lastName': supervisor.LAST_NAME.strip(), + 'bnumber': supervisor.ID.strip(), + 'department': supervisor.DEPT_NAME.strip(), + 'type': 'Supervisor'} + return dbToDict + +def currentAcademicYear(): + """ + Finds the current academic year + """ + today = date.today() + currentYear = today.year + if today.month < 7: + currentAcademicYear = (currentYear - 1, currentYear) + else: + currentAcademicYear = (currentYear, currentYear + 1) + # Note that the start of July is + # normally considered the start of a new academic year. + + return currentAcademicYear + +def getCurrentDepartment(org, account): + """Look up the department by org/account, stash it in session, or 404.""" + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except (NameError, DoesNotExist): + abort(404) + session['current_department_id'] = dept.departmentID + session['current_department'] = dept.DEPT_NAME + return dept + + +def getDepartmentMembers(dept): + """Supervisor-department rows for a department, as dicts.""" + return list( + SupervisorDepartment.select(SupervisorDepartment, Supervisor) + .where(SupervisorDepartment.department == dept) + .join(Supervisor) + .dicts() + ) + + +def getReleasedFormIds(): + """formIDs for labor release forms already in effect as of today.""" + today = date.today() + return ( + FormHistory.select(FormHistory.formID) + .join(LaborReleaseForm) + .where( + (FormHistory.historyType == "Labor Release Form") & + (FormHistory.status == "Approved") & + (LaborReleaseForm.releaseDate <= today) + ) + ) + + +def getStudentCounts(dept): + """Active/pending primary/secondary position counts, keyed by (dept, supervisor).""" + releasedForms = getReleasedFormIds() + + activePrimaries = (LaborStatusForm.jobType == 'Primary') & (LaborStatusForm.studentConfirmation == True) + pendingPrimaries = (LaborStatusForm.jobType == 'Primary') & (LaborStatusForm.studentConfirmation.is_null(True)) + activeSecondaries = (LaborStatusForm.jobType == 'Secondary') & (LaborStatusForm.studentConfirmation == True) + pendingSecondaries = (LaborStatusForm.jobType == 'Secondary') & (LaborStatusForm.studentConfirmation.is_null(True)) + + rows = list( + LaborStatusForm.select( + fn.SUM(Case(None, ((activePrimaries, 1),), 0)).alias("active_primary_positions"), + fn.SUM(Case(None, ((pendingPrimaries, 1),), 0)).alias("pending_primary_positions"), + fn.SUM(Case(None, ((activeSecondaries, 1),), 0)).alias("active_secondary_positions"), + fn.SUM(Case(None, ((pendingSecondaries, 1),), 0)).alias("pending_secondary_positions"), + LaborStatusForm.department, + LaborStatusForm.supervisor + ).where( + (LaborStatusForm.department == dept) & + (LaborStatusForm.laborStatusFormID.not_in(releasedForms)) + ).group_by( + LaborStatusForm.department, LaborStatusForm.supervisor + ).dicts() + ) + return {(row["department"], row["supervisor"]): row for row in rows} + + +def attachPositionCounts(members, counts): + """Merge counts onto each member dict, defaulting missing values to 0.""" + fields = [ + "active_primary_positions", "pending_primary_positions", + "active_secondary_positions", "pending_secondary_positions", + ] + for member in members: + row = counts.get((member["department"], member["supervisor"]), {}) + for field in fields: + member[field] = row.get(field, 0) + + return members + + diff --git a/app/logic/tracy.py b/app/logic/tracy.py index cd0ecf50a..027cd9fd5 100644 --- a/app/logic/tracy.py +++ b/app/logic/tracy.py @@ -142,6 +142,14 @@ def getStudentsFromBNumberSearch(self, bnum_part: str): students = STUDATA.query.filter(STUDATA.ID.like(bnum_search)).all() return students + def getSupervisorsFromBNumberSearch(self, bnum_part: str): + """ + Return a list of supervisors searching by bnumber + """ + bnum_search = bnum_part.strip().upper() + "%" + supervisors = STUSTAFF.query.filter(STUSTAFF.ID.like(bnum_search)).all() + return supervisors + def checkStudentOrSupervisor(self, username: str): """ Checks if the username belongs to a student or supervisor diff --git a/app/models/allocation.py b/app/models/allocation.py new file mode 100644 index 000000000..eb83877a0 --- /dev/null +++ b/app/models/allocation.py @@ -0,0 +1,23 @@ +from app.models import * +from app.models.department import Department +from app.models.supervisor import Supervisor +from app.models.term import Term + +class Allocation(baseModel): + termCode = ForeignKeyField(Term) + department = ForeignKeyField(Department) + isFinal = BooleanField(default=False) + approvedOn = DateField(null=True) + approvedBy = ForeignKeyField(Supervisor, null=True) + justification = TextField() + primary_10 = IntegerField() + primary_12 = IntegerField() + primary_15 = IntegerField() + primary_20 = IntegerField() + secondary_5 = IntegerField() + secondary_10 = IntegerField() + breakHours = IntegerField() + + class Meta: + indexes = ( (('termCode', 'department', 'isFinal'), True), ) + diff --git a/app/models/positionHistory.py b/app/models/positionHistory.py new file mode 100644 index 000000000..514670759 --- /dev/null +++ b/app/models/positionHistory.py @@ -0,0 +1,14 @@ +from app.models import * +from app.models.department import Department + +class PositionHistory(baseModel): + positionCode = CharField() + department = ForeignKeyField(Department) + status = CharField() + wls = IntegerField() + revisionDate = DateField() + description = TextField(default=None) + + class Meta: + indexes = ( (('positionCode', 'revisionDate', 'status'), True), ) + diff --git a/app/models/supervisor.py b/app/models/supervisor.py index 7c08354d3..3f9d3608e 100644 --- a/app/models/supervisor.py +++ b/app/models/supervisor.py @@ -13,9 +13,11 @@ class Supervisor(baseModel): ORG = CharField(null=True) DEPT_NAME = CharField(null=True) - legal_name = CharField(null=True) - preferred_name = CharField(null=True) - isActive = BooleanField(default=False) + legal_name = CharField(null=True) + preferred_name = CharField(null=True) + isActive = BooleanField(default=False) + isBanned = BooleanField(default=False) + @property diff --git a/app/models/supervisorDepartment.py b/app/models/supervisorDepartment.py index 3585e1eb2..dd5b15385 100644 --- a/app/models/supervisorDepartment.py +++ b/app/models/supervisorDepartment.py @@ -3,5 +3,6 @@ from app.models.department import Department class SupervisorDepartment(baseModel): - supervisor = ForeignKeyField(Supervisor, null=True) + supervisor = ForeignKeyField(Supervisor) department = ForeignKeyField(Department) + isCoordinator = BooleanField(default=False) diff --git a/app/static/css/base.css b/app/static/css/base.css index 81f010ff4..940f8d6e5 100755 --- a/app/static/css/base.css +++ b/app/static/css/base.css @@ -192,29 +192,75 @@ a { z-index:999; } -.site-footer { - position: fixed; - left: 280px; - right: 0; - bottom: 0; +.card { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; +} - background-color: #e9ebec; - color: #1e2433; +.card-body { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1rem 1rem; +} - padding: 8px 15px; - line-height: 1.4; - box-sizing: border-box; - z-index: 101; +.card-title { + margin-bottom: 0.5rem; + font-size: 1.25rem; + font-weight: 500; +} +.card-subtitle { + margin-top: -0.25rem; + margin-bottom: 0; } -.sidebar-push { - padding-bottom: 85px; +.card-text:last-child { + margin-bottom: 0; } -@media (max-width: 991px) { - .site-footer { - left: 0; - width: 100%; - } +.card-link + .card-link { + margin-left: 1rem; +} + +.card-header { + padding: 0.5rem 1rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-footer { + padding: 0.5rem 1rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-img, .card-img-top { + width: 100%; +} + +.card-img-top { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); } diff --git a/app/static/css/contributors.css b/app/static/css/contributors.css index 2f54c122e..0fe0601b0 100755 --- a/app/static/css/contributors.css +++ b/app/static/css/contributors.css @@ -3,6 +3,7 @@ color: #ffffff; padding: 7px 30px; margin-top: 100px; + margin-right: 250px; } .container { top: -75px; diff --git a/app/static/css/managemembers.css b/app/static/css/managemembers.css new file mode 100644 index 000000000..5283446cd --- /dev/null +++ b/app/static/css/managemembers.css @@ -0,0 +1,162 @@ +/*styles for column alignment in the data table*/ +.centered-header { + text-align: center !important; +} + +.centered-cell { + vertical-align: middle !important; +} + +/*The Supervisor Eligibility button*/ +.member-status-btn { + width: 80px; +} + +/* The add-member search bar*/ +#add-member { + display: flex; + justify-content: center; + margin-bottom: 20px; + z-index: 100; + gap: 0px; + height: 20px; +} + + +@media (min-width: 767.5px) { + #add-member{ + display: flex; + justify-content: flex-end; + gap: 0px; + position: relative; + top: 24px; + margin-bottom: 0px; + } +} + + +/* The switch container */ +.switch { + position: relative; + display: inline-block; + width: 31.25px; + height: 16.25px; + vertical-align: middle; +} + +/* Hide default HTML checkbox */ +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider track */ +.slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: #ccc !important; + transition: .4s; +} + +/* The slider knob */ +.slider:before { + position: absolute; + content: ""; + height: 11.25px; + width: 11.25px; + left: 2.5px; + bottom: 2.5px; + background-color: white !important; + transition: .4s; +} + +/* Checked state track color */ +input:checked + .slider { + background-color: #337ab7 !important; /* Bootstrap 3 Primary Color */ +} + +/* Checked state knob movement */ +input:checked + .slider:before { + transform: translateX(15px) !important; +} + +/* Rounded slider track and knob */ +.slider.round { + border-radius: 21.25px; +} +.slider.round:before { + border-radius: 50%; +} + +/* Optional label spacing */ +.switch-label { + margin-left: 6.25px; + vertical-align: middle; + font-weight: bold; +} + +/* hover indicator */ +a.hover-indicator { + text-decoration: none !important; + position: relative; + border-bottom: 1px dotted #000; + color: #000000; +} + +a.hover-indicator:visited, +a.hover-indicator:link, +a.hover-indicator:hover, +a.hover-indicator:active { + color: #000000; +} + +[class*="hover-indicator"]:after { + content: attr(aria-label); + display: none; + position: absolute; + top: 110%; + left: 10px; + z-index: 5000; + pointer-events: none; + padding: 8px 10px; + line-height: 15px; + white-space: nowrap; + text-decoration: none !important; + text-indent: 0; + overflow: visible; + font-size: .9em; + font-weight: normal; + -webkit-border-radius: 2px; + border-radius: 2px; + -webkit-box-shadow: 1px 2px 6px rgba(0,0,0,0.3); + box-shadow: 1px 2px 6px rgba(0,0,0,0.3); + background-color: #FFFFFF; + color: #000000; +} + +[class*="hover-indicator"]:hover:after, [class*="hover_indicator"]:focus:after { + display: block; + color: #000000; +} + +/*a "banned" badge displayed under the name of a banned department member*/ +.isbanned-badge { + position: relative; + display: inline-block; + margin-top: 3px; + padding: 2px 6px; + background: red; + color: white !important; + border-radius: 10px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.3px; + line-height: 1.2; +} + +/* switches inside the data table */ +.table-switch { + top: 10px; +} \ No newline at end of file diff --git a/app/static/css/managepositions.css b/app/static/css/managepositions.css new file mode 100644 index 000000000..9c40bbe38 --- /dev/null +++ b/app/static/css/managepositions.css @@ -0,0 +1,15 @@ +.width-12{ + width:12%; +} + +*{ + /* outline:solid 1px lime; */ + margin:0; + padding:0; + box-sizing:border-box; +} + +.department-header{ + padding:1%; + margin-top:-20px; +} \ No newline at end of file diff --git a/app/static/js/addSupervisorsToDepartment.js b/app/static/js/addSupervisorsToDepartment.js index 16c3f1d85..e69de29bb 100644 --- a/app/static/js/addSupervisorsToDepartment.js +++ b/app/static/js/addSupervisorsToDepartment.js @@ -1,31 +0,0 @@ -function addSupervisorToDepartment(supervisorID, departmentID, callback=() => {}) { - return $.ajax({ - method: "POST", - url: `/supervisorPortal/addUserToDept`, - data: {"supervisorID": supervisorID, "departmentID": departmentID}, - success: function(response) { - if (response == "True") { - msgFlash("Supervisor has been added to department.", "success") - clearDropdowns() - } else { - msgFlash("Supervisor is already a member of this department.", "warning") - clearDropdowns() - } - if (callback){ - callback() - } - }, - error: function() { - msgFlash("Failed to add supervisor, please try again.", "fail") - clearDropdowns() - }, - }) - -} - -function clearDropdowns(){ - $('select.selectpicker').each(function() { - $(`#${this.id} option:eq(0)`).prop("selected", true); - $(`#${this.id}`).selectpicker("refresh"); - }); -}; \ No newline at end of file diff --git a/app/static/js/departmentPortal.js b/app/static/js/departmentPortal.js new file mode 100644 index 000000000..cb2023a3a --- /dev/null +++ b/app/static/js/departmentPortal.js @@ -0,0 +1,6 @@ +$(document).ready(function() { + $("#selectedDepartment").on("change",function() { + deptData = $(this).find('option:selected').data(); + window.location = `/department/${deptData.org}/${deptData.account}`; + }); +}); diff --git a/app/static/js/manageMembers.js b/app/static/js/manageMembers.js new file mode 100644 index 000000000..c67b8b0ce --- /dev/null +++ b/app/static/js/manageMembers.js @@ -0,0 +1,221 @@ +$(document).ready(function() { + + $('#searchBoxContainer').children('.dropdown, .bootstrap-select, .form-control').addClass('open') + $('[type="search"], .form-control').focus(); + + $('#manageMembers').DataTable({ + 'columnDefs': [{ + 'targets': '.no-sorting', + 'orderable': false + }], // hide sort icon on header of first column + 'aaSorting': [ + [0, 'asc'] + ], // start to sort data in second column + searching: false, + pageLength: 10, + language: { + lengthMenu: " _MENU_ entries per page" + }, + //dom: '<"top"l>rt<"bottom"p><"clear">' + }); + $('[data-toggle="popover"]').popover(); + $(document).on("click", ".assign-coordinator", function() { + + let memberName = $(this).data("member-name"); + let supervisorID = $(this).data("supervisor"); + let isChecked = $(this).is(":checked"); + + $.ajax({ + url: "/members/coordinator_switch", + data: JSON.stringify({supervisorID: supervisorID, isCoordinator: isChecked}), + type: "POST", + contentType: "application/json", + success: function() { + if (isChecked) { + $("#flash_container").html("
" + memberName + " has been assigned as a coordinator.
"); + $("#flasher").delay(3000).fadeOut(); + } else { + $("#flash_container").html("
" + memberName + " is no longer a coordinator.
"); + $("#flasher").delay(3000).fadeOut(); + } + + }, + error: function() {console.log("An error has occured.");} + }) + + + }); + + $(document).on("click", ".member-status-btn", function() { + + let button = $(this); + let ban_badge = $(this).closest("tr").find(".isbanned-badge"); + + let memberName = button.data("member-name"); + let supervisorID = button.data("supervisor"); + + let banStatus = button.val(); + let isBanned = banStatus === "Ineligible" ; + + + $.ajax({ + url: "/members/ban_switch", + data: JSON.stringify({supervisorID: supervisorID, isBanned: isBanned}), + type: "POST", + contentType: "application/json", + success: function() { + if (!isBanned) { + category = "success"; + button.removeClass("btn-sucess").addClass("btn-danger"); + button.text("Ineligible"); + button.val("Ineligible"); + ban_badge.css("visibility", "visible"); + $("#flash_container").html("
" + memberName + " is no longer an eligible coordinator, so they cannot add students or create labor status forms .
"); + $("#flasher").delay(3000).fadeOut(); + } else { + category = "danger"; + button.removeClass("btn-danger").addClass("btn-success"); + button.html("Eligible"); + button.val("Eligible"); + ban_badge.css("visibility", "hidden"); + $("#flash_container").html("
" + memberName + " is an eligible coordinator now; they can create labor status forms and add new students.
"); + $("#flasher").delay(3000).fadeOut(); + } + + }, + error: function() {console.log("An error has occured.");} + }) + }); + + $(document).on("click", ".remove-member", function() { + + let redButton = $(this); + let row = $(this).closest("tr"); + + let memberName = redButton.data("member-name"); + let supervisorID = redButton.data("supervisor"); + + $.ajax({ + url: "/members/remove", + data: JSON.stringify({supervisorID: supervisorID}), + type: "DELETE", + contentType: "application/json", + success: function() { + $("#flash_container").html("
" + memberName + " has been removed from the department.
"); + $("#flasher").delay(3000).fadeOut(); + row.remove(); + }, + error: function() {console.log("An error has occured.");} + }) + }); +}) + +// Creates a dom fragment from html, rather than having to add dom elements +// https://love2dev.com/blog/inserting-html-using-createdocumentfragment-instead-of-using-jquery/ +function createFragment(htmlStr) { + let frag = document.createDocumentFragment(), temp = document.createElement('div'); + temp.innerHTML = htmlStr; + while(temp.firstChild) { frag.appendChild(temp.firstChild); } + return frag; +} + +// highlight search string. doesn't actually check for last name and first name, just highlights what we find +$.fn.selectpicker.Constructor.DEFAULTS.whiteList.mark = []; +function highlight(htmlStr, query) { + query = query.trim().split(" "); + for(i = 0; i < query.length; i++) { + htmlStr = htmlStr.replace(new RegExp(query[i], "gi"), function(match) { return `${match}`; }); + } + return htmlStr; +} + +let typeTimer; + +$('#search').selectpicker('refresh'); +$('.dropdown-menu .bs-searchbox input').on('keyup', function (e) { + // ignore arrow keys + if (e.keyCode == '40' || e.keyCode == '38') return; + + // wait a little longer for bnumber typing + keyInterval = 200 + if (e.keyCode >= 48 && e.keyCode <= 57) { + keyInterval = 500 + } + + // don't search for every key (especially relevant for bnumber) + clearTimeout(typeTimer) + typeTimer = setTimeout(function() { sendQuery(e.target.value); }, keyInterval) +}); + +$('#search').on('changed.bs.select', function () { + let supervisorID = $(this).val(); + let departmentID = $(this).data('department-id'); + + if (!supervisorID || !departmentID) return; + + addSupervisorToDepartment(supervisorID, departmentID, function() { + window.location.reload(); + }); +}); + +// We load the options returned into an html string and then add them to the selectpicker at the end, to save A LOT of time. +function sendQuery(search_str) { + $("#search").empty(); + $('#search').selectpicker("refresh"); + if (search_str.length >= 3) { + $.ajax({ + type: "GET", + url: "/members/search/" + encodeURIComponent(search_str), + contentType: 'application/json', + success: function(response) { + let optionString = "" + for (let key = 0; key < response.length; key++) { + let username = response[key]['username']; + let bnumber = response[key]['bnumber']; + let firstName = response[key]['firstName']; + let lastName = response[key]['lastName']; + let type = response[key]['type']; + if (type == "Supervisor") { + choice_text = bnumber + ': ' + firstName + ' ' + lastName; + highlighted_text = highlight(choice_text, search_str) + `${username}`; + optionString += ``; + } + } + $("#search").append(createFragment(optionString)) + $('#search').selectpicker("refresh"); + } + }); + } +} + +function addSupervisorToDepartment(supervisorID, departmentID, callback=() => {}) { + return $.ajax({ + method: "POST", + url: `/members/add`, + data: {"supervisorID": supervisorID, "departmentID": departmentID}, + success: function(response) { + if (response == "True") { + msgFlash("Supervisor has been added to department.", "success") + clearDropdowns(); + } else { + msgFlash("Supervisor is already a member of this department.", "warning") + clearDropdowns(); + } + if (callback){ + callback(); + } + }, + error: function() { + msgFlash("Failed to add supervisor, please try again.", "fail") + clearDropdowns(); + }, + }) + +} + +function clearDropdowns(){ + $('select.selectpicker').each(function() { + $(`#${this.id} option:eq(0)`).prop("selected", true); + $(`#${this.id}`).selectpicker("refresh"); + }); +}; \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 4b9825368..4d6c2a79f 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -66,20 +66,10 @@
{% block footer %} - + {% endblock %} + {% endblock %} diff --git a/app/templates/main/contributors.html b/app/templates/main/contributors.html index af33209dc..ffdfe4391 100755 --- a/app/templates/main/contributors.html +++ b/app/templates/main/contributors.html @@ -23,9 +23,9 @@

Contributors

{% endfor %} -
-
-
+
+ +
diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html new file mode 100644 index 000000000..696b9b0e3 --- /dev/null +++ b/app/templates/main/departmentPortal.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block scripts %} +{{super()}} + +{% endblock %} + +{% block app_content %} +

{% if department %} {{department.DEPT_NAME}} Portal {% else %} Choose a Department: {% endif %}

+ + +
+ + +
+ + +{% endblock %} diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html new file mode 100644 index 000000000..e0e49e918 --- /dev/null +++ b/app/templates/main/manageMembers.html @@ -0,0 +1,136 @@ +{% extends "base.html" %} + +{% block styles %} +{{super()}} + + + {% endblock %} + +{% block scripts %} +{{super()}} + + + +{% endblock %} + +{% block app_content %} +

{% if department %} Manage {{department.DEPT_NAME}} Members {% else %} Manage Members {% endif %}

+

Academic Year: {{currentAcademicYear[0]}}–{{currentAcademicYear[1]}}

+{%if currentSupervisor.isBanned %} + +

+ +{%endif%} +

+ +
+ Click to Skip +
+
+ +
+
+
+ +
+ + + + + + + + {% if currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent %} + + + {% endif %} + + + + + {% for i in members %} + + + + + + + + {% if currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent %} + + + {% endif %} + + + {% endfor %} + +
NameSuperviseesAssign Coordinator + Supervisor Eligibility +   + + Remove Member
+ {{ i['legal_name'] + " " + i['LAST_NAME']}} + + +
+ +
+ {% if (i['active_primary_positions'] == 0 and i['active_secondary_positions'] == 0) and (i['pending_primary_positions'] == 0 and i['pending_secondary_positions'] == 0) %} + None + {% else %} + Primary: {{i['active_primary_positions']}} active, {{i['pending_primary_positions']}} pending
+ Secondary: {{i['active_secondary_positions']}} active, {{i['pending_secondary_positions']}} pending + {% endif %} +
+
+
+ + +
+
+
+
+ {% if not i['isBanned'] %} + + {% else %} + + {% endif %} +
+
+
+ +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/main/managepositions.html b/app/templates/main/managepositions.html new file mode 100644 index 000000000..f61558a37 --- /dev/null +++ b/app/templates/main/managepositions.html @@ -0,0 +1,90 @@ +{% extends "base.html" %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% block scripts %} +{{super()}} + +{% endblock %} + +{% block app_content %} +
+

{{ department_name }}

+
+ +
+ + + +
+ + + + + + + + + + + + + + + {% for position in positions %} + + + + + + + + + {% endfor %} + +
Position (WSL)Position CodeStatusLast Revision DateView Position DescriptionEdit Position Description
{{position.POSN_TITLE}} ({{position.WLS}}){{position.POSN_CODE}} +

+ Active +

+
01/01/2024 + + + +
+
+

+ Total Positions: + {{ positions|length }} +

+
+ +{% endblock %} diff --git a/app/templates/main/supervisorPortal.html b/app/templates/main/supervisorPortal.html index 78383566c..3ab521a54 100644 --- a/app/templates/main/supervisorPortal.html +++ b/app/templates/main/supervisorPortal.html @@ -227,7 +227,7 @@

Form Search


-
+