diff --git a/app/__init__.py b/app/__init__.py index 11c90a0a..7c6483f2 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,9 +1,9 @@ import os - -from flask import Flask -from flask_restful import Api +from datetime import date +from flask import Flask, g, request, session from flask_bootstrap import Bootstrap -from playhouse.shortcuts import model_to_dict, dict_to_model +from flask_restful import Api +from playhouse.shortcuts import dict_to_model, model_to_dict app = Flask(__name__) @@ -55,17 +55,27 @@ def new_execute(*args, **kwargs): from app.controllers.api_routes.routes import initializeApiRoutes initializeApiRoutes(api) -from flask import g from app.models.user import User -from app.login_manager import require_login +from app.login_manager import getUsernameFromEnv, require_login @app.before_request def load_user(): - try: - g.currentUser = dict_to_model(User, session['currentUser']) + requestUsername = getUsernameFromEnv(request.environ) + try: + cachedUser = session['currentUser'] + + if cachedUser.get('username') == requestUsername: + g.currentUser = dict_to_model(User, cachedUser) + return + + session.pop('currentUser', None) + session.pop('username', None) + except Exception as e: - user = require_login() - session['currentUser'] = model_to_dict(user) - g.currentUser = user + pass + + user = require_login() + session['currentUser'] = model_to_dict(user) + g.currentUser = user from app.models.term import Term from app.login_manager import getOpenTerm @@ -79,6 +89,20 @@ def load_openTerm(): session['openTerm'] = model_to_dict(term) g.openTerm = term +def getCurrentYear(): + today = date.today() + year = today.year + + if today.month < 7: + return year - 1, year + + return year, year + 1 + +@app.before_request +def load_currentYear(): + g.currentYear = getCurrentYear() + + @app.context_processor def inject_environment(): return dict(env=app.config['ENV']) diff --git a/app/controllers/main_routes/__init__.py b/app/controllers/main_routes/__init__.py index 52853db7..5fc3dfec 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 index b0c33f5e..072648e5 100644 --- a/app/controllers/main_routes/departmentPortal.py +++ b/app/controllers/main_routes/departmentPortal.py @@ -1,10 +1,44 @@ -from flask import render_template, g +from flask import abort, g, jsonify, redirect, render_template, request, url_for +from peewee import DoesNotExist + from app.controllers.main_routes import main_bp from app.logic.getPositions import getPositions -from peewee import DoesNotExist +from app.logic.getSupervisors import buildSupervisorDisplay, getSupervisorDepartments +from app.logic.manageMembers import attachPositionCounts, getActivePendingPositionCounts +from app.logic.search import searchPerson from app.models.department import Department +from app.models.supervisor import Supervisor from app.models.supervisorDepartment import SupervisorDepartment + +@main_bp.route('/department///members', methods=['GET']) +def manageMembers(org=None, account=None): + """Generates the Manage Members page.""" + currentUser = g.currentUser + + if not currentUser.supervisor: + return redirect(url_for('main.laborhistory', id=currentUser.student.ID)) + + dept = Department.get_or_none(Department.ORG == org, Department.ACCOUNT == account) + + if not dept: + abort(404) + + departmentMembers = getSupervisorDepartments(dept) + supervisorDeptRecord = SupervisorDepartment.get_or_none(supervisor=currentUser.supervisor, department=dept) + + if not ( currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent or supervisorDeptRecord): + return render_template('errors/403.html'), 403 + + activePendingPositionCounts = getActivePendingPositionCounts(dept, g.currentYear) + departmentMembers = attachPositionCounts(departmentMembers, activePendingPositionCounts) + + return render_template( + 'main/manageMembers.html', + members=departmentMembers, + department=dept, + ) + @main_bp.route('/department///positions', methods=['GET']) def managePositions(org, account): try: @@ -22,7 +56,135 @@ def managePositions(org, account): positions = getPositions(dept) return render_template('main/managePositions.html', - department = dept, - department_name = dept.DEPT_NAME, - positions = positions + department=dept, + department_name=dept.DEPT_NAME, + positions=positions ) + + +@main_bp.route('/members/search/', methods=['GET']) +def searchMember(query=None): + """Search supervisors by name or B-number.""" + currentUser = g.currentUser + + if not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent or currentUser.supervisor ): + return render_template('errors/403.html'), 403 + + supervisors = ( + searchPerson(Supervisor, query) + .order_by(Supervisor.LAST_NAME.asc()) + .limit(10) + ) + + supervisors = [buildSupervisorDisplay(supervisor) for supervisor in supervisors] + supervisors = [supervisor for supervisor in supervisors if supervisor is not None] + + return jsonify(supervisors) + + +@main_bp.route('/members/update_coordinator', methods=['POST']) +def updateCoordinator(): + """Assigns or unassigns a supervisor as a Labor Coordinator.""" + currentUser = g.currentUser + + supervisorID = request.form.get("supervisorID") + departmentID = request.form.get("departmentID") + isCoordinator = request.form.get("isCoordinator") == "true" + + if not supervisorID or not departmentID: + return "", 400 + + supervisorDeptRecord = None + + if currentUser.supervisor: + supervisorDeptRecord = SupervisorDepartment.get_or_none( (SupervisorDepartment.supervisor == currentUser.supervisor) & (SupervisorDepartment.department == departmentID)) + + if not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent or supervisorDeptRecord ): + return render_template('errors/403.html'), 403 + + member = SupervisorDepartment.get((SupervisorDepartment.supervisor == supervisorID) & (SupervisorDepartment.department == departmentID)) + + member.isCoordinator = isCoordinator + member.save() + + return "", 200 + + +@main_bp.route('/members/update_eligibility', methods=['POST']) +def updateEligibility(): + """Updates a supervisor's eligibility status.""" + currentUser = g.currentUser + + if not ( currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent or currentUser.supervisor): + return render_template('errors/403.html'), 403 + + supervisorID = request.form.get("supervisorID") + + if not supervisorID: + return "", 400 + + 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.""" + currentUser = g.currentUser + + if not ( currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent or currentUser.supervisor): + return render_template('errors/403.html'), 403 + + supervisorID = request.form.get("supervisorID") + departmentID = request.form.get("departmentID") + + if not supervisorID or not departmentID: + return "", 400 + + member = SupervisorDepartment.get((SupervisorDepartment.supervisor == supervisorID) & (SupervisorDepartment.department == departmentID)) + + member.delete_instance() + + return "", 200 + + +@main_bp.route('/members/add', methods=['POST']) +def addUserToDept(): + """Adds a user to a department.""" + currentUser = g.currentUser + supervisorID = request.form.get("supervisorID") + departmentID = request.form.get("departmentID") + + if not supervisorID or not departmentID: + return jsonify(success=False, message="Missing supervisor or department."), 400 + + if not (currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent or currentUser.supervisor): + return render_template('errors/403.html'), 403 + + try: + supervisor = Supervisor.get_or_none(Supervisor.ID == supervisorID) + + if not supervisor: + return jsonify(success=False, message="Supervisor not found."), 404 + + supervisorDeptRecord = SupervisorDepartment.get_or_none( supervisor=supervisor, department=departmentID ) + + if supervisorDeptRecord: + return jsonify( + success=False, + message="Supervisor already exists in this department." + ), 200 + + SupervisorDepartment.create( supervisor=supervisor, department=departmentID) + + return jsonify(success=True, message="Supervisor added to department."), 200 + + except Exception as e: + print(f'Could not add user to department: {e}') + return jsonify( + success=False, + message="Could not add supervisor to department." + ), 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 0875988c..56990b91 100755 --- a/app/controllers/main_routes/main_routes.py +++ b/app/controllers/main_routes/main_routes.py @@ -59,10 +59,14 @@ def supervisorPortal(): @main_bp.route('/department//', methods=['GET']) def departmentPortal(org=None,account=None): currentUser = g.currentUser - try: - dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) - except (NameError, DoesNotExist): + if org and account: + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except (NameError, DoesNotExist): + dept = None + else: dept = None + if currentUser.isLaborAdmin: departments = list(Department.select().order_by(Department.isActive.desc(), Department.DEPT_NAME.asc())) diff --git a/app/logic/getSupervisors.py b/app/logic/getSupervisors.py index 508a43be..adcee1ff 100644 --- a/app/logic/getSupervisors.py +++ b/app/logic/getSupervisors.py @@ -1,43 +1,65 @@ +from app.logic.search import usernameFromEmail from app.models.supervisor import Supervisor from app.models.supervisorDepartment import SupervisorDepartment -from peewee import fn - def buildSupervisorDisplay(supervisor): - try: - firstName = supervisor.FIRST_NAME - lastName = supervisor.LAST_NAME - if not (firstName and lastName): - raise NameError("First name and last name are required") + """Build supervisor data used by portal displays and search results.""" + firstName = ( + supervisor.preferred_name or + supervisor.legal_name or + "" + ).strip() + lastName = (supervisor.LAST_NAME or "").strip() + email = supervisor.EMAIL.strip() if supervisor.EMAIL else None - except NameError as e: + if not firstName or not lastName: return None return { - "name": f"{firstName} {lastName}".strip(), - "email": supervisor.EMAIL + "name": f"{firstName} {lastName}", + "email": email, + "username": usernameFromEmail(email) if email else None, + "firstName": firstName, + "lastName": lastName, + "bnumber": supervisor.ID.strip(), + "department": (supervisor.DEPT_NAME or "").strip(), + "type": "Supervisor" } -def getSupervisors(dept): + + +def getSupervisorDepartments(dept): + """Return supervisor-department records for a department.""" + if dept is None: + return [] + + return list( + SupervisorDepartment + .select(SupervisorDepartment, Supervisor) + .join(Supervisor) + .where(SupervisorDepartment.department == dept) + .order_by(Supervisor.LAST_NAME.asc()) + ) + + +def getSupervisors(dept): laborCoordinators = [] supervisors = [] - - # Avoid querying department members unless the selected department exists. - if dept is not None: - supervisorDepartments = (SupervisorDepartment.select().join(Supervisor).where(SupervisorDepartment.department == dept) - .order_by(Supervisor.LAST_NAME.asc())) - for supervisorDepartment in supervisorDepartments: - supervisor = supervisorDepartment.supervisor - if supervisor is None: - continue - - supervisorDisplay = buildSupervisorDisplay(supervisor) - if not supervisorDisplay: - continue - - if supervisorDepartment.isCoordinator: - laborCoordinators.append(supervisorDisplay) - else: - supervisors.append(supervisorDisplay) + + for supervisorDepartment in getSupervisorDepartments(dept): + supervisor = supervisorDepartment.supervisor + + if supervisor is None: + continue + + supervisorDisplay = buildSupervisorDisplay(supervisor) + + if not supervisorDisplay: + continue + + if supervisorDepartment.isCoordinator: + laborCoordinators.append(supervisorDisplay) + else: + supervisors.append(supervisorDisplay) return supervisors, laborCoordinators \ No newline at end of file diff --git a/app/logic/manageMembers.py b/app/logic/manageMembers.py new file mode 100644 index 00000000..aa1bbfb7 --- /dev/null +++ b/app/logic/manageMembers.py @@ -0,0 +1,76 @@ +from datetime import date + +from peewee import Case, fn + +from app.models.formHistory import FormHistory +from app.models.laborReleaseForm import LaborReleaseForm +from app.models.laborStatusForm import LaborStatusForm +from app.models.term import Term + + +def getActivePendingPositionCounts(dept, currentYear): + """Active/pending primary/secondary position counts for the selected academic year.""" + today = date.today() + academicYearName = f"AY {currentYear[0]}-{currentYear[1]}" + pendingStatuses = ["Pending", "Pre-Student Approval"] + + releasedFormIds = ( + FormHistory + .select(FormHistory.formID) + .join(LaborReleaseForm) + .where( + (FormHistory.historyType == "Labor Release Form") & + (FormHistory.status == "Approved") & + (LaborReleaseForm.releaseDate <= today) + ) + ) + + activePrimaries = ((LaborStatusForm.jobType == "Primary") & (FormHistory.status == "Approved")) + pendingPrimaries = ((LaborStatusForm.jobType == "Primary") & (FormHistory.status.in_(pendingStatuses))) + activeSecondaries = ((LaborStatusForm.jobType == "Secondary") & (FormHistory.status == "Approved")) + pendingSecondaries = ((LaborStatusForm.jobType == "Secondary") & (FormHistory.status.in_(pendingStatuses))) + + rows = list( + LaborStatusForm + .select( + LaborStatusForm.department, + LaborStatusForm.supervisor, + 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"), + ) + .join(Term, on=(LaborStatusForm.termCode == Term.termCode)) + .switch(LaborStatusForm) + .join(FormHistory, on=(FormHistory.formID == LaborStatusForm.laborStatusFormID)) + .where( + (LaborStatusForm.department == dept) & + (Term.termName == academicYearName) & + (FormHistory.historyType == "Labor Status Form") & + (FormHistory.status.in_(["Approved"] + pendingStatuses)) & + (LaborStatusForm.laborStatusFormID.not_in(releasedFormIds)) + ) + .group_by(LaborStatusForm.department, LaborStatusForm.supervisor).dicts() + ) + + return { + (row["department"], row["supervisor"]): row + for row in rows + } + +def attachPositionCounts(members, counts): + """Attach position counts to each supervisor-department row.""" + fields = [ + "active_primary_positions", + "pending_primary_positions", + "active_secondary_positions", + "pending_secondary_positions", + ] + + for member in members: + row = counts.get((member.department_id, member.supervisor_id), {}) + + for field in fields: + setattr(member, field, row.get(field, 0)) + + return members \ No newline at end of file diff --git a/app/login_manager.py b/app/login_manager.py index 49f67016..5111b997 100755 --- a/app/login_manager.py +++ b/app/login_manager.py @@ -40,9 +40,10 @@ def require_login(): # Update the user's name user = updateUserFromTracy(user) - if 'username' not in session: + if session.get('username') != user.username: print("Logging in as", user.username) - session['username'] = user.username + + session['username'] = user.username return user diff --git a/app/static/css/manageMembers.css b/app/static/css/manageMembers.css new file mode 100644 index 00000000..b5e2821b --- /dev/null +++ b/app/static/css/manageMembers.css @@ -0,0 +1,176 @@ +#titleBlock { + margin-bottom: 15px; +} + +.manage-members-page { + max-width: 100%; + overflow-x: hidden; +} + +.table-responsive { + border: 0; + max-width: 100%; + overflow-x: hidden; +} + +#manageMembers_wrapper { + max-width: 100%; + overflow-x: hidden; +} + +#manageMembers { + width: 100% !important; + min-width: 0 !important; + table-layout: fixed; +} + +#manageMembers th, +#manageMembers td { + vertical-align: middle !important; + white-space: normal; + word-wrap: break-word; + overflow-wrap: break-word; +} + +#manageMembers th:nth-child(1), +#manageMembers td:nth-child(1) { + width: 16%; +} + +#manageMembers th:nth-child(2), +#manageMembers td:nth-child(2) { + width: 28%; +} + +#manageMembers th:nth-child(3), +#manageMembers td:nth-child(3) { + width: 18%; +} + +#manageMembers th:nth-child(4), +#manageMembers td:nth-child(4) { + width: 23%; +} + +#manageMembers th:nth-child(5), +#manageMembers td:nth-child(5) { + width: 15%; +} + +.switch { + position: relative; + display: inline-block; + width: 34px; + height: 18px; + vertical-align: middle; + margin: 0; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + inset: 0; + background-color: #ccc !important; + transition: 0.3s ease; +} + +.slider::before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 2px; + top: 2px; + background-color: #fff !important; + transition: 0.3s ease; +} + +.switch input:checked + .slider { + background-color: #337ab7 !important; +} + +.switch input:checked + .slider::before { + transform: translateX(16px); +} + +.slider.round { + border-radius: 18px; +} + +.slider.round::before { + border-radius: 50%; +} + +.table-switch { + top: 0; +} + +a.hover-indicator { + text-decoration: none !important; + position: relative; + border-bottom: 1px dotted #000; + color: #000; +} + +a.hover-indicator:visited, +a.hover-indicator:link, +a.hover-indicator:hover, +a.hover-indicator:active { + color: #000; +} + +[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: 0.9em; + font-weight: normal; + border-radius: 2px; + box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.3); + background-color: #fff; + color: #000; +} + +[class*="hover-indicator"]:hover::after, +[class*="hover-indicator"]:focus::after { + display: block; + color: #000; +} + +.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; +} + +.position-counts { + display: inline-grid; + grid-template-columns: max-content 1fr; + column-gap: 6px; + row-gap: 2px; + white-space: normal; +} \ No newline at end of file diff --git a/app/static/js/addSupervisorsToDepartment.js b/app/static/js/addSupervisorsToDepartment.js index 16c3f1d8..0ca6c978 100644 --- a/app/static/js/addSupervisorsToDepartment.js +++ b/app/static/js/addSupervisorsToDepartment.js @@ -1,26 +1,27 @@ function addSupervisorToDepartment(supervisorID, departmentID, callback=() => {}) { return $.ajax({ method: "POST", - url: `/supervisorPortal/addUserToDept`, + 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() - } + msgFlash(response.message, response.success ? "success" : "warning"); + clearDropdowns(); + if (callback){ - callback() + callback(); } }, - error: function() { - msgFlash("Failed to add supervisor, please try again.", "fail") - clearDropdowns() + error: function(response) { + let message = "Failed to add supervisor, please try again."; + + if (response.responseJSON && response.responseJSON.message) { + message = response.responseJSON.message; + } + + msgFlash(message, "fail"); + clearDropdowns(); }, }) - } function clearDropdowns(){ diff --git a/app/static/js/manageMembers.js b/app/static/js/manageMembers.js new file mode 100644 index 00000000..8615f141 --- /dev/null +++ b/app/static/js/manageMembers.js @@ -0,0 +1,177 @@ +$(document).ready(function() { + + $('#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(); + setTimeout(function() { $(".alert-danger").fadeOut();}, 5000); + $(document).on("click", ".assign-coordinator", function() { + + let memberName = $(this).data("member-name"); + let supervisorID = $(this).data("supervisor"); + let departmentID = $(this).data("department"); + let isChecked = $(this).is(":checked"); + + $.ajax({ + url: "/members/update_coordinator", + data: { supervisorID: supervisorID, departmentID: departmentID, isCoordinator: isChecked }, + type: "POST", + 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("change", ".member-status-toggle", function() { + let toggle = $(this); + let banBadge = toggle.closest("tr").find(".isbanned-badge"); + + let memberName = toggle.data("member-name"); + let supervisorID = toggle.data("supervisor"); + let isIneligible = toggle.is(":checked"); + + $.ajax({ + url: "/members/update_eligibility", + data: { supervisorID: supervisorID }, + type: "POST", + success: function() { + if (isIneligible) { + banBadge.css("visibility", "visible"); + $("#flash_container").html("
" + memberName + " is no longer eligible.
"); + } else { + banBadge.css("visibility", "hidden"); + $("#flash_container").html("
" + memberName + " is eligible now.
"); + } + + $("#flasher").delay(3000).fadeOut(); + }, + error: function() { + toggle.prop("checked", !isIneligible); + console.log("An error has occurred."); + } + }); + }); + + $(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"); + let departmentID = redButton.data("department"); + + + $.ajax({ + url: "/members/remove", + data: { supervisorID: supervisorID, departmentID: departmentID }, + type: "DELETE", + success: function() { + $("#flash_container").html("
" + memberName + " has been removed from the department.
"); + $("#flasher").delay(3000).fadeOut(); + row.remove(); + }, + + }) + }); +}) + +// 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( let 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 + let 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") { + let choice_text = bnumber + ': ' + firstName + ' ' + lastName; + let highlighted_text = highlight(choice_text, search_str) + `${username}`; + optionString += ``; + } + } + $("#search").append(createFragment(optionString)) + $('#search').selectpicker("refresh"); + } + }); + } +} \ No newline at end of file diff --git a/app/static/js/supervisorPortal.js b/app/static/js/supervisorPortal.js index a4810ebb..e62046cc 100644 --- a/app/static/js/supervisorPortal.js +++ b/app/static/js/supervisorPortal.js @@ -25,10 +25,6 @@ $(document).ready(function () { $('#sortOptions').show(); }); - $('#addUserToDept').on('click', function () { - $("#addSupervisorToDeptModal").modal("show"); - $('#addUser').prop('disabled', true) - }) $("#sortByButton").on('click', function () { var isDisabled = $('#fieldPicker').prop('disabled'); if (!isDisabled && $('#fieldPicker').val() == '') { @@ -43,16 +39,6 @@ $(document).ready(function () { $('.selectpicker').selectpicker('refresh') } - $('#addUser').on('click', function () { - let supervisorID = $('#supervisorModalSelect :selected').val() - let departmentID = $('#departmentModalSelect :selected').val() - - addSupervisorToDepartment(supervisorID, departmentID) - }) - - $('#departmentModalSelect').on('change', disableButtonHandler) - $('#supervisorModalSelect').on('change', disableButtonHandler) - $('#clearSelectionsButton').on('click', function () { $("input:checkbox").removeAttr("checked"); clearDropdowns() diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html index e7786c85..a39ec11b 100644 --- a/app/templates/main/departmentPortal.html +++ b/app/templates/main/departmentPortal.html @@ -36,7 +36,7 @@

{% if department %} {{department.DEPT_NAME}} Portal {% e

Insert Allocations Card Here

@@ -109,7 +109,7 @@

{% if supervisors|length <= 1 %} Supervisor{% else %} Supervisors{% endi {% endif %} diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html new file mode 100644 index 00000000..d0ae89c8 --- /dev/null +++ b/app/templates/main/manageMembers.html @@ -0,0 +1,155 @@ +{% 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: {{ g.currentYear[0] }}–{{ g.currentYear[1] }}

+{%if g.currentUser.supervisor.isBanned %} + +

+ +{%endif%} +

+ +
+
+
+ Click to Skip + +
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + {% for member in members %} + + + + + + + + + + + + {% endfor %} + +
NameSuperviseesAssign Coordinator + Supervisor Eligibility +   + + + Remove Member
+ + {{ member.supervisor.legal_name }} {{ member.supervisor.LAST_NAME }} + + + + + + +
+ + + Ineligible + +
+ {% if (member.active_primary_positions == 0 and member.active_secondary_positions == 0) and (member.pending_primary_positions == 0 and member.pending_secondary_positions == 0) %} + None + {% else %} +
+ Primary: + {{ member.active_primary_positions }} active, {{ member.pending_primary_positions }} pending + + Secondary: + {{ member.active_secondary_positions }} active, {{ member.pending_secondary_positions }} pending +
+ {% endif %} +
+ + + + + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/main/supervisorPortal.html b/app/templates/main/supervisorPortal.html index 78383566..5e3c2672 100644 --- a/app/templates/main/supervisorPortal.html +++ b/app/templates/main/supervisorPortal.html @@ -28,8 +28,6 @@ src="{{url_for('static', filename='js/allPendingForms.js')}}?u={{lastStaticUpdate}}"> -