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..4df0f59fb --- /dev/null +++ b/app/controllers/main_routes/departmentPortal.py @@ -0,0 +1,69 @@ +from flask import render_template, request, json, redirect, url_for, send_file, g, flash, jsonify +from peewee import JOIN, DoesNotExist, fn, Case +from functools import reduce +import operator +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.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 + + +@main_bp.route('/department///members', methods=['GET']) +def manageStaff(org=None,account=None): + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except (NameError, DoesNotExist): + dept = None + abort(404) + + members = list( + SupervisorDepartment. + select( + SupervisorDepartment, + Supervisor + ).where( + SupervisorDepartment.department == dept + ).join( + Supervisor + ).dicts() + ) + + student_count = list( + LaborStatusForm. + select( + fn.SUM(Case(LaborStatusForm.jobType, (("Primary", 1),), 0)).alias("primary_positions"), + fn.SUM(Case(LaborStatusForm.jobType, (("Secondary", 1),), 0)).alias("secondary_positions"), + LaborStatusForm.department, + LaborStatusForm.supervisor + ).group_by( + LaborStatusForm.department, + LaborStatusForm.supervisor + ).dicts() + ) + + + counts = {(row["department"], row["supervisor"]): row for row in student_count} + + for member in members: + + key = (member["department"], member["supervisor"]) + row = counts.get(key, {}) + + member["primary_positions"] = row.get("primary_positions", 0) + member["secondary_positions"] = row.get("secondary_positions", 0) + + return render_template('main/manageMembers.html', + members = members, + department = dept) + diff --git a/app/controllers/main_routes/main_routes.py b/app/controllers/main_routes/main_routes.py index b3af06c9e..20be46822 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,7 @@ 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 @main_bp.route('/logout', methods=['GET']) def triggerLogout(): @@ -47,6 +48,57 @@ def supervisorPortal(): currentUser = currentUser ) +@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())) + + pos = Tracy().getPositionsFromDepartment(org, account) + positions = [] + for i in pos: + positions.append(i.POSN_TITLE + "" + "(" + i.WLS + ")") + + staff = Tracy().getSupervisors() + supervisors = [] + + for i in staff: + if i.ORG == org: + supervisors.append(i.FIRST_NAME + " " + i.LAST_NAME + " (" + i.EMAIL + ")") + + return render_template('main/departmentPortal.html', + departments = departments, + department = dept, + positions = positions, + supervisors = supervisors + ) + +@main_bp.route('/department///managepositions', methods=['GET']) +def managePositions(org, account): + try: + dept = Department.get(Department.ORG == org, Department.ACCOUNT == account) + except DoesNotExist: + return render_template('errors/404.html'), 404 + + 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/addUserToDept', methods=['GET', 'POST']) def addUserToDept(): userDeptData = request.form @@ -113,4 +165,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/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 916fb4db9..940f8d6e5 100755 --- a/app/static/css/base.css +++ b/app/static/css/base.css @@ -191,3 +191,76 @@ a { font-size:1.2em; z-index:999; } + +.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; +} + +.card-body { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1rem 1rem; +} + +.card-title { + margin-bottom: 0.5rem; + font-size: 1.25rem; + font-weight: 500; +} + +.card-subtitle { + margin-top: -0.25rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.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/managemembers.css b/app/static/css/managemembers.css new file mode 100644 index 000000000..3384fddc6 --- /dev/null +++ b/app/static/css/managemembers.css @@ -0,0 +1,154 @@ +/*styles for column alignment in the data table*/ +.centered-header { + text-align: center !important; +} + +.centered-cell { + vertical-align: middle !important; +} + +/* The add-member search bar*/ +#add-member { + display: flex; + justify-content: center; + gap: 0px; + height: 20px; + margin-bottom: 30px; +} + + +@media (min-width: 767px) { + #add-member{ + display: flex; + justify-content: flex-end; + gap: 0px; + /*margin-bottom: -20px;*/ + } +} + + +/* 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/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..4f3a3cf49 --- /dev/null +++ b/app/static/js/manageMembers.js @@ -0,0 +1,44 @@ +$(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">' + }); + + $(document).on("click", ".member-status-btn", function() { + let memberName = $(this).data("member-name"); + let memberStatus = $(this).data("member-status"); + let category; + +if (memberStatus === "Banned") { + category = "danger"; +} else { + category = "success"; +} + let quote = String.fromCharCode(39); + + $("#flash_container").html("
The status for " + memberName + " has been set to " + quote + memberStatus + quote + ".
"); + $("#flasher").delay(3000).fadeOut(); + + if (memberStatus === "Banned") { + $(this).removeClass("btn-danger").addClass("btn-success"); + $(this).text("Unban"); + $(this).data("member-status", "Unbanned"); + } else { + $(this).removeClass("btn-success").addClass("btn-danger"); + $(this).html("  Ban  "); + $(this).data("member-status", "Banned"); + } + }); +}) + diff --git a/app/templates/main/departmentPortal.html b/app/templates/main/departmentPortal.html new file mode 100644 index 000000000..e1abe22ff --- /dev/null +++ b/app/templates/main/departmentPortal.html @@ -0,0 +1,158 @@ +{% extends "base.html" %} + +{% block scripts %} +{{super()}} + + +{% endblock %} +{% block app_content %} +

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

+ + + +
+ + +
+ +{% if department %} +
+ +
+
+
+ + + + + Current Allocation +
+


AY 26/27 15/20 POSITIONS

Break Hours 0 of 200 hrs

+ Go somewhere +
+
+ +
+
+
+
+ +
+
+

+ Members +

+
+
+

Labor Coordinator(s)

+

Dr. Jones

+ +

Supervisors

+ {% for s in supervisors %} + {% if 3 > loop.index0 %} +

{{ s }}

+ {% elif 4 > loop.index0 %} +

and {{ supervisors | length}} more...

+ {% endif %} + {% endfor %} +
+ + +
+ +
+
+
+
+

Positions

+
+
+ +
+ +
+
    + {% for p in positions %} + {% if 7 > loop.index0 %} +
  • {{ p }}
  • + {% elif 8 == loop.index0 %} +

and {{ positions | length}} more...

+ {% endif %} + {% endfor %} + + +
+ +
+
+ +{% endif %} + + + + +{% endblock %}\ + + + + diff --git a/app/templates/main/manageMembers.html b/app/templates/main/manageMembers.html new file mode 100644 index 000000000..c2f995354 --- /dev/null +++ b/app/templates/main/manageMembers.html @@ -0,0 +1,110 @@ +{% 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 %}

+ +

+ +
+ Click to Skip +
+
+ +
+
+
+ +
+ + + + + + + + {% if currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent %} + + + {% endif %} + + + + + {% for i in members %} + + + + + + + + {% if currentUser.isLaborAdmin or currentUser.isLaborDepartmentStudent %} + + + {% endif %} + + + {% endfor %} + +
NameSuperviseesAssign CoordinatorChange StatusRemove Member
+ {{ i['legal_name'] + " " + i['LAST_NAME']}} + + +
+ + {% if i[4] %} + Banned + {% endif %} +
+ {% if i['primary_positions'] == 0 and i['secondary_positions'] == 0 %} + None + {% else %} + Primary: {{i['primary_positions']}}
+ Secondary: {{i['secondary_positions']}} + {% endif %} +
+
+ + +
+
+ {% if not i[4] %} + + {% 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


-
+