04/09 update: redesign user's roles
This commit is contained in:
+2
-2
@@ -99,9 +99,9 @@ def create_app(config_name='default'):
|
|||||||
unread = Notification.query.filter_by(
|
unread = Notification.query.filter_by(
|
||||||
user_id=current_user.id, is_read=False
|
user_id=current_user.id, is_read=False
|
||||||
).count()
|
).count()
|
||||||
# Pending verification count — only computed for supervisor+ roles
|
# Pending verification count — only computed for director+ roles
|
||||||
pv_count = 0
|
pv_count = 0
|
||||||
if current_user.role in ('admin', 'supervisor'):
|
if current_user.role in ('admin', 'director'):
|
||||||
pv_count = Issue.query.filter_by(
|
pv_count = Issue.query.filter_by(
|
||||||
status='pending_verification'
|
status='pending_verification'
|
||||||
).count()
|
).count()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ One row per (event_type, role_key) pair.
|
|||||||
role_key values
|
role_key values
|
||||||
---------------
|
---------------
|
||||||
admin — all users with role='admin'
|
admin — all users with role='admin'
|
||||||
supervisor — all users with role='supervisor'
|
director — all users with role='director'
|
||||||
inspector — all users with role='inspector'
|
inspector — all users with role='inspector'
|
||||||
project_manager — all users with role='project_manager'
|
project_manager — all users with role='project_manager'
|
||||||
customer — all customer-portal users assigned to the relevant facility
|
customer — all customer-portal users assigned to the relevant facility
|
||||||
@@ -18,16 +18,16 @@ custom — free-form extra email addresses stored in custom_emails JSO
|
|||||||
|
|
||||||
Default matrix (mirrors current hardcoded behaviour)
|
Default matrix (mirrors current hardcoded behaviour)
|
||||||
-----------------------------------------------------
|
-----------------------------------------------------
|
||||||
inspection_completed : admin ✓ supervisor ✓ inspector ✗ pm ✗ customer ✓
|
inspection_completed : admin ✓ director ✓ inspector ✗ pm ✗ customer ✓
|
||||||
issue_assigned : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
issue_assigned : admin ✗ director ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
||||||
issue_status : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
issue_status : admin ✗ director ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
||||||
issue_comment : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
issue_comment : admin ✗ director ✗ inspector ✗ pm ✗ customer ✗ (assignee implicit)
|
||||||
issue_follow_update : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✗ (followers implicit)
|
issue_follow_update : admin ✗ director ✗ inspector ✗ pm ✗ customer ✗ (followers implicit)
|
||||||
issue_flagged : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✓ (assignee implicit)
|
issue_flagged : admin ✗ director ✗ inspector ✗ pm ✗ customer ✓ (assignee implicit)
|
||||||
issue_created : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✓ (assignee implicit)
|
issue_created : admin ✗ director ✗ inspector ✗ pm ✗ customer ✓ (assignee implicit)
|
||||||
issue_updated_customer : admin ✗ supervisor ✗ inspector ✗ pm ✗ customer ✓
|
issue_updated_customer : admin ✗ director ✗ inspector ✗ pm ✗ customer ✓
|
||||||
verification_requested : admin ✓ supervisor ✓ inspector ✗ pm ✗ customer ✗
|
verification_requested : admin ✓ director ✓ inspector ✗ pm ✗ customer ✗
|
||||||
sla_alert : admin ✓ supervisor ✗ inspector ✗ pm ✗ customer ✗ (assignee + followers implicit)
|
sla_alert : admin ✓ director ✗ inspector ✗ pm ✗ customer ✗ (assignee + followers implicit)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -36,7 +36,7 @@ from app import db
|
|||||||
# Role keys available in the matrix UI
|
# Role keys available in the matrix UI
|
||||||
MATRIX_ROLES = [
|
MATRIX_ROLES = [
|
||||||
('admin', 'Admin'),
|
('admin', 'Admin'),
|
||||||
('supervisor', 'Supervisor'),
|
('director', 'Director'),
|
||||||
('inspector', 'Inspector'),
|
('inspector', 'Inspector'),
|
||||||
('project_manager', 'Project Manager'),
|
('project_manager', 'Project Manager'),
|
||||||
('customer', 'Customer'),
|
('customer', 'Customer'),
|
||||||
@@ -65,84 +65,84 @@ MATRIX_EVENTS = {
|
|||||||
MATRIX_DEFAULTS = {
|
MATRIX_DEFAULTS = {
|
||||||
# inspection_completed
|
# inspection_completed
|
||||||
('inspection_completed', 'admin'): True,
|
('inspection_completed', 'admin'): True,
|
||||||
('inspection_completed', 'supervisor'): True,
|
('inspection_completed', 'director'): True,
|
||||||
('inspection_completed', 'inspector'): False,
|
('inspection_completed', 'inspector'): False,
|
||||||
('inspection_completed', 'project_manager'): False,
|
('inspection_completed', 'project_manager'): False,
|
||||||
('inspection_completed', 'customer'): True,
|
('inspection_completed', 'customer'): True,
|
||||||
('inspection_completed', 'custom'): False,
|
('inspection_completed', 'custom'): False,
|
||||||
# issue_assigned (assignee is always notified implicitly)
|
# issue_assigned (assignee is always notified implicitly)
|
||||||
('issue_assigned', 'admin'): False,
|
('issue_assigned', 'admin'): False,
|
||||||
('issue_assigned', 'supervisor'): False,
|
('issue_assigned', 'director'): False,
|
||||||
('issue_assigned', 'inspector'): False,
|
('issue_assigned', 'inspector'): False,
|
||||||
('issue_assigned', 'project_manager'): False,
|
('issue_assigned', 'project_manager'): False,
|
||||||
('issue_assigned', 'customer'): False,
|
('issue_assigned', 'customer'): False,
|
||||||
('issue_assigned', 'custom'): False,
|
('issue_assigned', 'custom'): False,
|
||||||
# issue_reassigned
|
# issue_reassigned
|
||||||
('issue_reassigned', 'admin'): False,
|
('issue_reassigned', 'admin'): False,
|
||||||
('issue_reassigned', 'supervisor'): False,
|
('issue_reassigned', 'director'): False,
|
||||||
('issue_reassigned', 'inspector'): False,
|
('issue_reassigned', 'inspector'): False,
|
||||||
('issue_reassigned', 'project_manager'): False,
|
('issue_reassigned', 'project_manager'): False,
|
||||||
('issue_reassigned', 'customer'): False,
|
('issue_reassigned', 'customer'): False,
|
||||||
('issue_reassigned', 'custom'): False,
|
('issue_reassigned', 'custom'): False,
|
||||||
# issue_unassigned
|
# issue_unassigned
|
||||||
('issue_unassigned', 'admin'): False,
|
('issue_unassigned', 'admin'): False,
|
||||||
('issue_unassigned', 'supervisor'): False,
|
('issue_unassigned', 'director'): False,
|
||||||
('issue_unassigned', 'inspector'): False,
|
('issue_unassigned', 'inspector'): False,
|
||||||
('issue_unassigned', 'project_manager'): False,
|
('issue_unassigned', 'project_manager'): False,
|
||||||
('issue_unassigned', 'customer'): False,
|
('issue_unassigned', 'customer'): False,
|
||||||
('issue_unassigned', 'custom'): False,
|
('issue_unassigned', 'custom'): False,
|
||||||
# issue_status
|
# issue_status
|
||||||
('issue_status', 'admin'): False,
|
('issue_status', 'admin'): False,
|
||||||
('issue_status', 'supervisor'): False,
|
('issue_status', 'director'): False,
|
||||||
('issue_status', 'inspector'): False,
|
('issue_status', 'inspector'): False,
|
||||||
('issue_status', 'project_manager'): False,
|
('issue_status', 'project_manager'): False,
|
||||||
('issue_status', 'customer'): False,
|
('issue_status', 'customer'): False,
|
||||||
('issue_status', 'custom'): False,
|
('issue_status', 'custom'): False,
|
||||||
# issue_comment
|
# issue_comment
|
||||||
('issue_comment', 'admin'): False,
|
('issue_comment', 'admin'): False,
|
||||||
('issue_comment', 'supervisor'): False,
|
('issue_comment', 'director'): False,
|
||||||
('issue_comment', 'inspector'): False,
|
('issue_comment', 'inspector'): False,
|
||||||
('issue_comment', 'project_manager'): False,
|
('issue_comment', 'project_manager'): False,
|
||||||
('issue_comment', 'customer'): False,
|
('issue_comment', 'customer'): False,
|
||||||
('issue_comment', 'custom'): False,
|
('issue_comment', 'custom'): False,
|
||||||
# issue_follow_update (followers always notified implicitly)
|
# issue_follow_update (followers always notified implicitly)
|
||||||
('issue_follow_update', 'admin'): False,
|
('issue_follow_update', 'admin'): False,
|
||||||
('issue_follow_update', 'supervisor'): False,
|
('issue_follow_update', 'director'): False,
|
||||||
('issue_follow_update', 'inspector'): False,
|
('issue_follow_update', 'inspector'): False,
|
||||||
('issue_follow_update', 'project_manager'): False,
|
('issue_follow_update', 'project_manager'): False,
|
||||||
('issue_follow_update', 'customer'): False,
|
('issue_follow_update', 'customer'): False,
|
||||||
('issue_follow_update', 'custom'): False,
|
('issue_follow_update', 'custom'): False,
|
||||||
# issue_flagged (from inspection)
|
# issue_flagged (from inspection)
|
||||||
('issue_flagged', 'admin'): False,
|
('issue_flagged', 'admin'): False,
|
||||||
('issue_flagged', 'supervisor'): False,
|
('issue_flagged', 'director'): False,
|
||||||
('issue_flagged', 'inspector'): False,
|
('issue_flagged', 'inspector'): False,
|
||||||
('issue_flagged', 'project_manager'): False,
|
('issue_flagged', 'project_manager'): False,
|
||||||
('issue_flagged', 'customer'): True,
|
('issue_flagged', 'customer'): True,
|
||||||
('issue_flagged', 'custom'): False,
|
('issue_flagged', 'custom'): False,
|
||||||
# issue_created (standalone)
|
# issue_created (standalone)
|
||||||
('issue_created', 'admin'): False,
|
('issue_created', 'admin'): False,
|
||||||
('issue_created', 'supervisor'): False,
|
('issue_created', 'director'): False,
|
||||||
('issue_created', 'inspector'): False,
|
('issue_created', 'inspector'): False,
|
||||||
('issue_created', 'project_manager'): False,
|
('issue_created', 'project_manager'): False,
|
||||||
('issue_created', 'customer'): True,
|
('issue_created', 'customer'): True,
|
||||||
('issue_created', 'custom'): False,
|
('issue_created', 'custom'): False,
|
||||||
# issue_updated_customer
|
# issue_updated_customer
|
||||||
('issue_updated_customer', 'admin'): False,
|
('issue_updated_customer', 'admin'): False,
|
||||||
('issue_updated_customer', 'supervisor'): False,
|
('issue_updated_customer', 'director'): False,
|
||||||
('issue_updated_customer', 'inspector'): False,
|
('issue_updated_customer', 'inspector'): False,
|
||||||
('issue_updated_customer', 'project_manager'): False,
|
('issue_updated_customer', 'project_manager'): False,
|
||||||
('issue_updated_customer', 'customer'): True,
|
('issue_updated_customer', 'customer'): True,
|
||||||
('issue_updated_customer', 'custom'): False,
|
('issue_updated_customer', 'custom'): False,
|
||||||
# verification_requested
|
# verification_requested
|
||||||
('verification_requested', 'admin'): True,
|
('verification_requested', 'admin'): True,
|
||||||
('verification_requested', 'supervisor'): True,
|
('verification_requested', 'director'): True,
|
||||||
('verification_requested', 'inspector'): False,
|
('verification_requested', 'inspector'): False,
|
||||||
('verification_requested', 'project_manager'): False,
|
('verification_requested', 'project_manager'): False,
|
||||||
('verification_requested', 'customer'): False,
|
('verification_requested', 'customer'): False,
|
||||||
('verification_requested', 'custom'): False,
|
('verification_requested', 'custom'): False,
|
||||||
# sla_alert (assignee + followers always notified implicitly)
|
# sla_alert (assignee + followers always notified implicitly)
|
||||||
('sla_alert', 'admin'): True,
|
('sla_alert', 'admin'): True,
|
||||||
('sla_alert', 'supervisor'): False,
|
('sla_alert', 'director'): False,
|
||||||
('sla_alert', 'inspector'): False,
|
('sla_alert', 'inspector'): False,
|
||||||
('sla_alert', 'project_manager'): False,
|
('sla_alert', 'project_manager'): False,
|
||||||
('sla_alert', 'customer'): False,
|
('sla_alert', 'customer'): False,
|
||||||
|
|||||||
+4
-1
@@ -16,7 +16,10 @@ class User(UserMixin, db.Model):
|
|||||||
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
||||||
password_hash = db.Column(db.String(255), nullable=False)
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
role = db.Column(
|
role = db.Column(
|
||||||
db.Enum('admin', 'supervisor', 'inspector', 'project_manager', 'customer'),
|
# 'supervisor' retained temporarily so the Enum is valid before the
|
||||||
|
# migration UPDATE runs. The migration removes it after all rows are
|
||||||
|
# updated to 'director'.
|
||||||
|
db.Enum('admin', 'supervisor', 'director', 'inspector', 'project_manager', 'customer'),
|
||||||
nullable=False
|
nullable=False
|
||||||
)
|
)
|
||||||
created_at = db.Column(db.DateTime, default=now_eastern)
|
created_at = db.Column(db.DateTime, default=now_eastern)
|
||||||
|
|||||||
+6
-6
@@ -4,7 +4,7 @@ from urllib.parse import urlparse
|
|||||||
from app import db
|
from app import db
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.utils.forms import LoginForm, UserForm, ProfileForm
|
from app.utils.forms import LoginForm, UserForm, ProfileForm
|
||||||
from app.utils.decorators import admin_required
|
from app.utils.decorators import admin_required, supervisor_required
|
||||||
import logging
|
import logging
|
||||||
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE, ACTION_LOGIN, ACTION_LOGOUT
|
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE, ACTION_LOGIN, ACTION_LOGOUT
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ def profile():
|
|||||||
|
|
||||||
@bp.route('/users')
|
@bp.route('/users')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def list_users():
|
def list_users():
|
||||||
# Exclude customer accounts — those are managed exclusively via /customers
|
# Exclude customer accounts — those are managed exclusively via /customers
|
||||||
users = (
|
users = (
|
||||||
@@ -143,7 +143,7 @@ def list_users():
|
|||||||
|
|
||||||
@bp.route('/users/new', methods=['GET', 'POST'])
|
@bp.route('/users/new', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def create_user():
|
def create_user():
|
||||||
form = UserForm()
|
form = UserForm()
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ def create_user():
|
|||||||
|
|
||||||
@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
|
@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def edit_user(user_id):
|
def edit_user(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
form = UserForm(user=user, obj=user)
|
form = UserForm(user=user, obj=user)
|
||||||
@@ -196,7 +196,7 @@ def edit_user(user_id):
|
|||||||
|
|
||||||
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
|
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def delete_user(user_id):
|
def delete_user(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ def delete_user(user_id):
|
|||||||
|
|
||||||
@bp.route('/users/<int:user_id>/toggle-active', methods=['POST'])
|
@bp.route('/users/<int:user_id>/toggle-active', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def toggle_active(user_id):
|
def toggle_active(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -20,7 +20,7 @@ from app.models.user import User
|
|||||||
from app.models.project import Project, CustomerAssignment
|
from app.models.project import Project, CustomerAssignment
|
||||||
from app.models.facility import Facility
|
from app.models.facility import Facility
|
||||||
from app.utils.forms import CustomerUserForm, CustomerAssignmentForm, CustomerInviteForm, SetPasswordForm
|
from app.utils.forms import CustomerUserForm, CustomerAssignmentForm, CustomerInviteForm, SetPasswordForm
|
||||||
from app.utils.decorators import admin_required
|
from app.utils.decorators import admin_required, supervisor_required
|
||||||
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE
|
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE
|
||||||
from app.utils.scope import get_customer_scope
|
from app.utils.scope import get_customer_scope
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ bp = Blueprint('customers', __name__, url_prefix='/customers')
|
|||||||
|
|
||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def index():
|
def index():
|
||||||
"""Consolidated customer management dashboard."""
|
"""Consolidated customer management dashboard."""
|
||||||
customers = (
|
customers = (
|
||||||
@@ -101,7 +101,7 @@ def index():
|
|||||||
|
|
||||||
@bp.route('/new', methods=['GET', 'POST'])
|
@bp.route('/new', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def create():
|
def create():
|
||||||
"""Create a customer account via email invitation.
|
"""Create a customer account via email invitation.
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ def _send_invite_email(user, token):
|
|||||||
|
|
||||||
@bp.route('/<int:customer_id>/resend-invite', methods=['POST'])
|
@bp.route('/<int:customer_id>/resend-invite', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def resend_invite(customer_id):
|
def resend_invite(customer_id):
|
||||||
"""Generate a fresh token and resend the set-password invitation email."""
|
"""Generate a fresh token and resend the set-password invitation email."""
|
||||||
customer = User.query.get_or_404(customer_id)
|
customer = User.query.get_or_404(customer_id)
|
||||||
@@ -295,7 +295,7 @@ def set_password(token):
|
|||||||
|
|
||||||
@bp.route('/<int:customer_id>/edit', methods=['GET', 'POST'])
|
@bp.route('/<int:customer_id>/edit', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def edit(customer_id):
|
def edit(customer_id):
|
||||||
customer = User.query.get_or_404(customer_id)
|
customer = User.query.get_or_404(customer_id)
|
||||||
if customer.role != 'customer':
|
if customer.role != 'customer':
|
||||||
@@ -326,7 +326,7 @@ def edit(customer_id):
|
|||||||
|
|
||||||
@bp.route('/<int:customer_id>')
|
@bp.route('/<int:customer_id>')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def manage(customer_id):
|
def manage(customer_id):
|
||||||
"""Single-customer detail page: profile + all assignments."""
|
"""Single-customer detail page: profile + all assignments."""
|
||||||
customer = User.query.get_or_404(customer_id)
|
customer = User.query.get_or_404(customer_id)
|
||||||
@@ -363,7 +363,7 @@ def manage(customer_id):
|
|||||||
|
|
||||||
@bp.route('/<int:customer_id>/assignments/add', methods=['POST'])
|
@bp.route('/<int:customer_id>/assignments/add', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def add_assignment(customer_id):
|
def add_assignment(customer_id):
|
||||||
customer = User.query.get_or_404(customer_id)
|
customer = User.query.get_or_404(customer_id)
|
||||||
if customer.role != 'customer':
|
if customer.role != 'customer':
|
||||||
@@ -412,7 +412,7 @@ def add_assignment(customer_id):
|
|||||||
|
|
||||||
@bp.route('/assignments/<int:assignment_id>/remove', methods=['POST'])
|
@bp.route('/assignments/<int:assignment_id>/remove', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def remove_assignment(assignment_id):
|
def remove_assignment(assignment_id):
|
||||||
assignment = CustomerAssignment.query.get_or_404(assignment_id)
|
assignment = CustomerAssignment.query.get_or_404(assignment_id)
|
||||||
customer_id = assignment.user_id
|
customer_id = assignment.user_id
|
||||||
@@ -437,7 +437,7 @@ def remove_assignment(assignment_id):
|
|||||||
|
|
||||||
@bp.route('/<int:customer_id>/toggle-active', methods=['POST'])
|
@bp.route('/<int:customer_id>/toggle-active', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def toggle_active(customer_id):
|
def toggle_active(customer_id):
|
||||||
customer = User.query.get_or_404(customer_id)
|
customer = User.query.get_or_404(customer_id)
|
||||||
if customer.role != 'customer':
|
if customer.role != 'customer':
|
||||||
@@ -463,7 +463,7 @@ def toggle_active(customer_id):
|
|||||||
|
|
||||||
@bp.route('/import/template')
|
@bp.route('/import/template')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def import_template():
|
def import_template():
|
||||||
"""Download a blank CSV template showing the expected import format."""
|
"""Download a blank CSV template showing the expected import format."""
|
||||||
import csv, io
|
import csv, io
|
||||||
@@ -495,7 +495,7 @@ def import_template():
|
|||||||
|
|
||||||
@bp.route('/import', methods=['GET', 'POST'])
|
@bp.route('/import', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def bulk_import():
|
def bulk_import():
|
||||||
"""Two-phase CSV import for customer accounts.
|
"""Two-phase CSV import for customer accounts.
|
||||||
|
|
||||||
@@ -744,7 +744,7 @@ def bulk_import():
|
|||||||
|
|
||||||
@bp.route('/facilities-for-project/<int:project_id>')
|
@bp.route('/facilities-for-project/<int:project_id>')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@supervisor_required
|
||||||
def facilities_for_project(project_id):
|
def facilities_for_project(project_id):
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
project = Project.query.get_or_404(project_id)
|
project = Project.query.get_or_404(project_id)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def index():
|
|||||||
thirty_days_ago = now - timedelta(days=30)
|
thirty_days_ago = now - timedelta(days=30)
|
||||||
|
|
||||||
is_inspector = current_user.role == 'inspector'
|
is_inspector = current_user.role == 'inspector'
|
||||||
is_privileged = current_user.role in ['admin', 'supervisor']
|
is_privileged = current_user.role in ['admin', 'director']
|
||||||
is_customer = current_user.role == 'customer'
|
is_customer = current_user.role == 'customer'
|
||||||
is_project_manager = current_user.role == 'project_manager'
|
is_project_manager = current_user.role == 'project_manager'
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ def index():
|
|||||||
followup_q = followup_q.filter(False)
|
followup_q = followup_q.filter(False)
|
||||||
pending_followups = followup_q.count()
|
pending_followups = followup_q.count()
|
||||||
|
|
||||||
# ── System stats (admin/supervisor) ───────────────────────────────────
|
# ── System stats (admin/director) ────────────────────────────────────────
|
||||||
total_facilities = Facility.query.filter_by(active=True).count() if is_privileged else 0
|
total_facilities = Facility.query.filter_by(active=True).count() if is_privileged else 0
|
||||||
total_templates = InspectionTemplate.query.count() if is_privileged else 0
|
total_templates = InspectionTemplate.query.count() if is_privileged else 0
|
||||||
total_users = User.query.count() if current_user.role == 'admin' else 0
|
total_users = User.query.count() if current_user.role == 'admin' else 0
|
||||||
|
|||||||
@@ -673,7 +673,7 @@ def flag_issue(inspection_id):
|
|||||||
|
|
||||||
form = IssueForm()
|
form = IssueForm()
|
||||||
areas = Area.query.filter_by(facility_id=inspection.facility_id).order_by(Area.name).all()
|
areas = Area.query.filter_by(facility_id=inspection.facility_id).order_by(Area.name).all()
|
||||||
staff = User.query.filter(User.role.in_(['supervisor', 'inspector'])).order_by(User.username).all()
|
staff = User.query.filter(User.role.in_(['director', 'inspector'])).order_by(User.username).all()
|
||||||
|
|
||||||
form.area_id.choices = [(a.id, a.name) for a in areas]
|
form.area_id.choices = [(a.id, a.name) for a in areas]
|
||||||
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
|
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ def view(issue_id):
|
|||||||
return redirect(url_for('issues.index'))
|
return redirect(url_for('issues.index'))
|
||||||
|
|
||||||
form = IssueUpdateForm(obj=issue)
|
form = IssueUpdateForm(obj=issue)
|
||||||
staff = User.query.filter(User.role.in_(['supervisor','inspector'])).order_by(User.username).all()
|
staff = User.query.filter(User.role.in_(['director','inspector'])).order_by(User.username).all()
|
||||||
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
|
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
|
||||||
form.status.data = form.status.data or issue.status
|
form.status.data = form.status.data or issue.status
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ def view(issue_id):
|
|||||||
|
|
||||||
issue.status = form.status.data
|
issue.status = form.status.data
|
||||||
|
|
||||||
if current_user.role in ['admin', 'supervisor']:
|
if current_user.role in ['admin', 'director']:
|
||||||
issue.assigned_to = form.assigned_to.data or None
|
issue.assigned_to = form.assigned_to.data or None
|
||||||
|
|
||||||
if form.status.data == 'resolved' and not issue.resolved_at:
|
if form.status.data == 'resolved' and not issue.resolved_at:
|
||||||
@@ -353,7 +353,7 @@ def unfollow(issue_id):
|
|||||||
def create():
|
def create():
|
||||||
form = IssueForm()
|
form = IssueForm()
|
||||||
areas = Area.query.join(Facility).filter(Facility.active == True).order_by(Facility.name, Area.name).all()
|
areas = Area.query.join(Facility).filter(Facility.active == True).order_by(Facility.name, Area.name).all()
|
||||||
staff = User.query.filter(User.role.in_(['supervisor','inspector'])).order_by(User.username).all()
|
staff = User.query.filter(User.role.in_(['director','inspector'])).order_by(User.username).all()
|
||||||
|
|
||||||
form.area_id.choices = [(a.id, f"{a.facility.name} — {a.name}") for a in areas]
|
form.area_id.choices = [(a.id, f"{a.facility.name} — {a.name}") for a in areas]
|
||||||
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
|
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
|
||||||
@@ -460,16 +460,16 @@ def verify(issue_id):
|
|||||||
@bp.route('/<int:issue_id>/request-verification', methods=['POST'])
|
@bp.route('/<int:issue_id>/request-verification', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def request_verification(issue_id):
|
def request_verification(issue_id):
|
||||||
"""Inspector/assignee marks the issue as pending supervisor verification."""
|
"""Inspector/assignee marks the issue as pending director verification."""
|
||||||
issue = Issue.query.get_or_404(issue_id)
|
issue = Issue.query.get_or_404(issue_id)
|
||||||
|
|
||||||
if current_user.role == 'customer':
|
if current_user.role == 'customer':
|
||||||
flash('Access denied.', 'danger')
|
flash('Access denied.', 'danger')
|
||||||
return redirect(url_for('issues.index'))
|
return redirect(url_for('issues.index'))
|
||||||
|
|
||||||
# Only the assignee, supervisor, or admin can request verification
|
# Only the assignee, director, or admin can request verification
|
||||||
can_act = (
|
can_act = (
|
||||||
current_user.role in ['admin', 'supervisor']
|
current_user.role in ['admin', 'director']
|
||||||
or issue.assigned_to == current_user.id
|
or issue.assigned_to == current_user.id
|
||||||
)
|
)
|
||||||
if not can_act:
|
if not can_act:
|
||||||
@@ -556,7 +556,7 @@ def verification_queue():
|
|||||||
def delete(issue_id):
|
def delete(issue_id):
|
||||||
"""Permanently delete an issue and its associated photos.
|
"""Permanently delete an issue and its associated photos.
|
||||||
|
|
||||||
Restricted to admin and supervisor roles. The deletion is recorded in
|
Restricted to admin and director roles. The deletion is recorded in
|
||||||
the audit log before the record is removed so there is always a trace.
|
the audit log before the record is removed so there is always a trace.
|
||||||
"""
|
"""
|
||||||
issue = Issue.query.get_or_404(issue_id)
|
issue = Issue.query.get_or_404(issue_id)
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ def _date_range():
|
|||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
# Customers get a scoped view; internal staff need supervisor+ access
|
# Customers get a scoped view; internal staff need director+ access
|
||||||
if current_user.role not in ['admin', 'supervisor', 'project_manager', 'customer']:
|
if current_user.role not in ['admin', 'director', 'project_manager', 'customer']:
|
||||||
from flask import flash, redirect, url_for
|
from flask import flash, redirect, url_for
|
||||||
flash('Access denied.', 'danger')
|
flash('Access denied.', 'danger')
|
||||||
return redirect(url_for('dashboard.index'))
|
return redirect(url_for('dashboard.index'))
|
||||||
@@ -179,7 +179,7 @@ def index():
|
|||||||
@bp.route('/facility/<int:facility_id>')
|
@bp.route('/facility/<int:facility_id>')
|
||||||
@login_required
|
@login_required
|
||||||
def facility_report(facility_id):
|
def facility_report(facility_id):
|
||||||
if current_user.role not in ['admin', 'supervisor', 'project_manager', 'customer']:
|
if current_user.role not in ['admin', 'director', 'project_manager', 'customer']:
|
||||||
from flask import flash, redirect, url_for
|
from flask import flash, redirect, url_for
|
||||||
flash('Access denied.', 'danger')
|
flash('Access denied.', 'danger')
|
||||||
return redirect(url_for('dashboard.index'))
|
return redirect(url_for('dashboard.index'))
|
||||||
@@ -228,7 +228,7 @@ def facility_report(facility_id):
|
|||||||
def facility_scorecard(facility_id):
|
def facility_scorecard(facility_id):
|
||||||
"""Comprehensive per-facility scorecard: score trend, SLA compliance,
|
"""Comprehensive per-facility scorecard: score trend, SLA compliance,
|
||||||
issue breakdown by severity, inspection frequency."""
|
issue breakdown by severity, inspection frequency."""
|
||||||
if current_user.role not in ['admin', 'supervisor', 'project_manager', 'customer']:
|
if current_user.role not in ['admin', 'director', 'project_manager', 'customer']:
|
||||||
from flask import flash, redirect, url_for
|
from flask import flash, redirect, url_for
|
||||||
flash('Access denied.', 'danger')
|
flash('Access denied.', 'danger')
|
||||||
return redirect(url_for('dashboard.index'))
|
return redirect(url_for('dashboard.index'))
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
<strong>{{ entry.username }}</strong>
|
<strong>{{ entry.username }}</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'supervisor' %}warning{% else %}info{% endif %} bg-opacity-75">
|
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'director' %}warning{% else %}info{% endif %} bg-opacity-75">
|
||||||
{{ entry.user_role | title }}
|
{{ entry.user_role | title }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
|
|
||||||
<dt class="col-sm-4 text-muted">Role at Time</dt>
|
<dt class="col-sm-4 text-muted">Role at Time</dt>
|
||||||
<dd class="col-sm-8">
|
<dd class="col-sm-8">
|
||||||
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'supervisor' %}warning{% else %}info{% endif %}">
|
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'director' %}warning{% else %}info{% endif %}">
|
||||||
{{ entry.user_role | title }}
|
{{ entry.user_role | title }}
|
||||||
</span>
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
<p class="text-muted mb-1" style="font-size:.85rem;">@{{ current_user.username }}</p>
|
<p class="text-muted mb-1" style="font-size:.85rem;">@{{ current_user.username }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="text-muted mb-2">{{ current_user.email }}</p>
|
<p class="text-muted mb-2">{{ current_user.email }}</p>
|
||||||
<span class="badge fs-6 bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'supervisor' %}warning{% else %}info{% endif %}">
|
<span class="badge fs-6 bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'director' %}warning{% else %}info{% endif %}">
|
||||||
<i class="bi bi-{% if current_user.role == 'admin' %}shield-fill{% elif current_user.role == 'supervisor' %}star-fill{% else %}person-badge-fill{% endif %} me-1"></i>
|
<i class="bi bi-{% if current_user.role == 'admin' %}shield-fill{% elif current_user.role == 'director' %}star-fill{% else %}person-badge-fill{% endif %} me-1"></i>
|
||||||
{{ current_user.role | title }}
|
{{ current_user.role | title }}
|
||||||
</span>
|
</span>
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
{% block title %}User Management{% endblock %}
|
{% block title %}User Management{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-4 align-items-center">
|
<div class="row mb-4">
|
||||||
<div class="col">
|
<div class="col-md-6">
|
||||||
<h2><i class="bi bi-people-fill"></i> Internal User Management</h2>
|
<h2><i class="bi bi-people-fill"></i> Internal User Management</h2>
|
||||||
<p class="text-muted mb-0">Manage staff accounts (Admin, Supervisor, Inspector, Project Manager). For customer accounts, visit <a href="{{ url_for('customers.index') }}">Customer Management</a>.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-md-6 text-end">
|
||||||
<a href="{{ url_for('auth.create_user') }}" class="btn btn-primary">
|
<a href="{{ url_for('auth.create_user') }}" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Add User
|
<i class="bi bi-plus-circle"></i> Add User
|
||||||
</a>
|
</a>
|
||||||
@@ -37,7 +36,7 @@
|
|||||||
<td>{{ user.full_name or '—' }}</td>
|
<td>{{ user.full_name or '—' }}</td>
|
||||||
<td>{{ user.email }}</td>
|
<td>{{ user.email }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-{% if user.role == 'admin' %}danger{% elif user.role == 'supervisor' %}warning{% elif user.role == 'project_manager' %}primary{% elif user.role == 'customer' %}success{% else %}info{% endif %}">
|
<span class="badge bg-{% if user.role == 'admin' %}danger{% elif user.role == 'director' %}warning{% elif user.role == 'project_manager' %}primary{% elif user.role == 'customer' %}success{% else %}info{% endif %}">
|
||||||
{{ user.role.replace('_',' ')|title }}
|
{{ user.role.replace('_',' ')|title }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('dashboard.index') }}">Dashboard</a>
|
<a class="nav-link" href="{{ url_for('dashboard.index') }}">Dashboard</a>
|
||||||
</li>
|
</li>
|
||||||
{% if current_user.role in ['admin', 'supervisor', 'project_manager'] %}
|
{% if current_user.role in ['admin', 'director', 'project_manager'] %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('projects.index') }}">Projects</a>
|
<a class="nav-link" href="{{ url_for('projects.index') }}">Projects</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('facilities.list_facilities') }}">Facilities</a>
|
<a class="nav-link" href="{{ url_for('facilities.list_facilities') }}">Facilities</a>
|
||||||
</li>
|
</li>
|
||||||
{% if current_user.role != 'customer' %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('templates.index') }}">Templates</a>
|
<a class="nav-link" href="{{ url_for('templates.index') }}">Templates</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('reports.index') }}">Reports</a>
|
<a class="nav-link" href="{{ url_for('reports.index') }}">Reports</a>
|
||||||
</li>
|
</li>
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('scheduled_reports.index') }}">Scheduled Reports</a>
|
<a class="nav-link" href="{{ url_for('scheduled_reports.index') }}">Scheduled Reports</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('issues.index') }}">Issues</a>
|
<a class="nav-link" href="{{ url_for('issues.index') }}">Issues</a>
|
||||||
</li>
|
</li>
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link d-flex align-items-center gap-1"
|
<a class="nav-link d-flex align-items-center gap-1"
|
||||||
href="{{ url_for('issues.verification_queue') }}">
|
href="{{ url_for('issues.verification_queue') }}">
|
||||||
@@ -142,13 +142,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.role == 'admin' %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('auth.list_users') }}">Users</a>
|
<a class="nav-link" href="{{ url_for('auth.list_users') }}">Users</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('customers.index') }}">Customers</a>
|
<a class="nav-link" href="{{ url_for('customers.index') }}">Customers</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.role == 'admin' %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('audit.index') }}">Audit Trail</a>
|
<a class="nav-link" href="{{ url_for('audit.index') }}">Audit Trail</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="row mb-3 align-items-center">
|
<div class="row mb-3 align-items-center">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2 class="mb-0">Welcome, {{ current_user.username }}!</h2>
|
<h2 class="mb-0">Welcome, {{ current_user.username }}!</h2>
|
||||||
<span class="badge bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'supervisor' %}warning{% elif current_user.role == 'project_manager' %}primary{% elif current_user.role == 'customer' %}success{% else %}info{% endif %} mt-1">
|
<span class="badge bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'director' %}warning{% elif current_user.role == 'project_manager' %}primary{% elif current_user.role == 'customer' %}success{% else %}info{% endif %} mt-1">
|
||||||
{{ current_user.role.replace('_',' ')|title }}
|
{{ current_user.role.replace('_',' ')|title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,8 +171,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# ── System quick stats (admin/supervisor) ──────────────────────────────── #}
|
{# ── System quick stats (admin/director) ─────────────────────────────────── #}
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-6 col-md-{% if current_user.role == 'admin' %}4{% else %}6{% endif %}">
|
<div class="col-6 col-md-{% if current_user.role == 'admin' %}4{% else %}6{% endif %}">
|
||||||
<div class="card shadow-sm text-center">
|
<div class="card shadow-sm text-center">
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
|
|
||||||
{# ── Score trend chart + Facility performance ───────────────────────────── #}
|
{# ── Score trend chart + Facility performance ───────────────────────────── #}
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-lg-{% if current_user.role in ['admin','supervisor'] and facility_perf %}7{% else %}12{% endif %}">
|
<div class="col-lg-{% if current_user.role in ['admin','director'] and facility_perf %}7{% else %}12{% endif %}">
|
||||||
<div class="card shadow-sm h-100">
|
<div class="card shadow-sm h-100">
|
||||||
<div class="card-header bg-light fw-semibold">
|
<div class="card-header bg-light fw-semibold">
|
||||||
<i class="bi bi-graph-up me-1"></i>Inspection Score Trend (Last 30 Days)
|
<i class="bi bi-graph-up me-1"></i>Inspection Score Trend (Last 30 Days)
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.role in ['admin', 'supervisor'] and facility_perf %}
|
{% if current_user.role in ['admin', 'director'] and facility_perf %}
|
||||||
<div class="col-lg-5">
|
<div class="col-lg-5">
|
||||||
<div class="card shadow-sm h-100">
|
<div class="card shadow-sm h-100">
|
||||||
<div class="card-header bg-light fw-semibold">
|
<div class="card-header bg-light fw-semibold">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<h2><i class="bi bi-building"></i> Facilities</h2>
|
<h2><i class="bi bi-building"></i> Facilities</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 text-end">
|
<div class="col-md-6 text-end">
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<a href="{{ url_for('facilities.create_facility') }}" class="btn btn-primary">
|
<a href="{{ url_for('facilities.create_facility') }}" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Add Facility
|
<i class="bi bi-plus-circle"></i> Add Facility
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
<h2><i class="bi bi-building"></i> {{ facility.name }}</h2>
|
<h2><i class="bi bi-building"></i> {{ facility.name }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end d-flex gap-2 justify-content-end align-items-start">
|
<div class="col-md-4 text-end d-flex gap-2 justify-content-end align-items-start">
|
||||||
{% if current_user.role in ['admin', 'supervisor', 'project_manager', 'customer'] %}
|
{% if current_user.role in ['admin', 'director', 'project_manager', 'customer'] %}
|
||||||
<a href="{{ url_for('reports.facility_report', facility_id=facility.id) }}"
|
<a href="{{ url_for('reports.facility_report', facility_id=facility.id) }}"
|
||||||
class="btn btn-outline-info">
|
class="btn btn-outline-info">
|
||||||
<i class="bi bi-graph-up-arrow"></i> Scorecard
|
<i class="bi bi-graph-up-arrow"></i> Scorecard
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<a href="{{ url_for('facilities.edit_facility', facility_id=facility.id) }}" class="btn btn-outline-primary">
|
<a href="{{ url_for('facilities.edit_facility', facility_id=facility.id) }}" class="btn btn-outline-primary">
|
||||||
<i class="bi bi-pencil"></i> Edit
|
<i class="bi bi-pencil"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ area.inspections.count() }}</td>
|
<td>{{ area.inspections.count() }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<a href="{{ url_for('facilities.edit_area', area_id=area.id) }}" class="btn btn-sm btn-outline-primary">
|
<a href="{{ url_for('facilities.edit_area', area_id=area.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('inspections.view', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-secondary">View</a>
|
<a href="{{ url_for('inspections.view', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-secondary">View</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-outline-danger ms-1"
|
class="btn btn-sm btn-outline-danger ms-1"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<!-- Delete Inspection Confirmation Modal -->
|
<!-- Delete Inspection Confirmation Modal -->
|
||||||
<div class="modal fade" id="deleteInspectionModal" tabindex="-1" aria-labelledby="deleteInspectionModalLabel" aria-hidden="true">
|
<div class="modal fade" id="deleteInspectionModal" tabindex="-1" aria-labelledby="deleteInspectionModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const modal = document.getElementById('deleteInspectionModal');
|
const modal = document.getElementById('deleteInspectionModal');
|
||||||
|
|||||||
@@ -356,7 +356,7 @@
|
|||||||
<i class="bi bi-arrow-repeat"></i> Re-inspect
|
<i class="bi bi-arrow-repeat"></i> Re-inspect
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.role in ['admin','supervisor'] %}
|
{% if current_user.role in ['admin','director'] %}
|
||||||
{% if not inspection.follow_up_required %}
|
{% if not inspection.follow_up_required %}
|
||||||
<button type="button" class="btn btn-sm btn-outline-warning"
|
<button type="button" class="btn btn-sm btn-outline-warning"
|
||||||
data-bs-toggle="modal" data-bs-target="#followupModal"
|
data-bs-toggle="modal" data-bs-target="#followupModal"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2><i class="bi bi-exclamation-triangle"></i> Issues</h2>
|
<h2><i class="bi bi-exclamation-triangle"></i> Issues</h2>
|
||||||
{% if current_user.role in ['admin','supervisor'] %}
|
{% if current_user.role in ['admin','director'] %}
|
||||||
<a href="{{ url_for('issues.create') }}" class="btn btn-danger">
|
<a href="{{ url_for('issues.create') }}" class="btn btn-danger">
|
||||||
<i class="bi bi-plus-circle"></i> Log Issue
|
<i class="bi bi-plus-circle"></i> Log Issue
|
||||||
</a>
|
</a>
|
||||||
@@ -100,13 +100,13 @@
|
|||||||
|
|
||||||
<a href="{{ url_for('issues.view', issue_id=issue.id) }}"
|
<a href="{{ url_for('issues.view', issue_id=issue.id) }}"
|
||||||
class="btn btn-sm btn-outline-secondary">
|
class="btn btn-sm btn-outline-secondary">
|
||||||
{% if current_user.role in ['admin','supervisor'] or issue.assigned_to == current_user.id %}
|
{% if current_user.role in ['admin','director'] or issue.assigned_to == current_user.id %}
|
||||||
<i class="bi bi-pencil"></i> Edit
|
<i class="bi bi-pencil"></i> Edit
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-eye"></i> View
|
<i class="bi bi-eye"></i> View
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<form method="POST" action="{{ url_for('issues.delete', issue_id=issue.id) }}"
|
<form method="POST" action="{{ url_for('issues.delete', issue_id=issue.id) }}"
|
||||||
class="d-inline"
|
class="d-inline"
|
||||||
onsubmit="return confirm('Permanently delete Issue #{{ issue.id }}? This cannot be undone.');">
|
onsubmit="return confirm('Permanently delete Issue #{{ issue.id }}? This cannot be undone.');">
|
||||||
|
|||||||
@@ -83,8 +83,8 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<div class="alert alert-info py-2 mb-0">
|
<div class="alert alert-info py-2 mb-0">
|
||||||
<i class="bi bi-hourglass-split me-1"></i>
|
<i class="bi bi-hourglass-split me-1"></i>
|
||||||
<strong>Awaiting supervisor verification.</strong>
|
<strong>Awaiting director verification.</strong>
|
||||||
{% if current_user.role in ['admin','supervisor'] %}
|
{% if current_user.role in ['admin','director'] %}
|
||||||
<form method="POST" action="{{ url_for('issues.verify', issue_id=issue.id) }}" class="mt-2">
|
<form method="POST" action="{{ url_for('issues.verify', issue_id=issue.id) }}" class="mt-2">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ── Update Form ────────────────────────────────────────────────────── #}
|
{# ── Update Form ────────────────────────────────────────────────────── #}
|
||||||
{% set can_edit = current_user.role in ['admin','supervisor'] or issue.assigned_to == current_user.id %}
|
{% set can_edit = current_user.role in ['admin','director'] or issue.assigned_to == current_user.id %}
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-light"><h6 class="mb-0">Update Issue</h6></div>
|
<div class="card-header bg-light"><h6 class="mb-0">Update Issue</h6></div>
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
{{ form.status.label(class="form-label fw-semibold") }}
|
{{ form.status.label(class="form-label fw-semibold") }}
|
||||||
{{ form.status(class="form-select") }}
|
{{ form.status(class="form-select") }}
|
||||||
</div>
|
</div>
|
||||||
{% if current_user.role in ['admin','supervisor'] %}
|
{% if current_user.role in ['admin','director'] %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ form.assigned_to.label(class="form-label fw-semibold") }}
|
{{ form.assigned_to.label(class="form-label fw-semibold") }}
|
||||||
{{ form.assigned_to(class="form-select") }}
|
{{ form.assigned_to(class="form-select") }}
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
class="mt-2">
|
class="mt-2">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="btn btn-outline-info w-100"
|
<button type="submit" class="btn btn-outline-info w-100"
|
||||||
onclick="return confirm('Mark this issue as pending supervisor verification?')">
|
onclick="return confirm('Mark this issue as pending director verification?')">
|
||||||
<i class="bi bi-hourglass-split me-1"></i> Request Verification
|
<i class="bi bi-hourglass-split me-1"></i> Request Verification
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
<a href="{{ url_for('issues.index') }}" class="btn btn-outline-secondary btn-sm">
|
<a href="{{ url_for('issues.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="bi bi-arrow-left"></i> Back to Issues
|
<i class="bi bi-arrow-left"></i> Back to Issues
|
||||||
</a>
|
</a>
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||||
data-bs-toggle="modal" data-bs-target="#deleteIssueModal">
|
data-bs-toggle="modal" data-bs-target="#deleteIssueModal">
|
||||||
<i class="bi bi-trash"></i> Delete Issue
|
<i class="bi bi-trash"></i> Delete Issue
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
<div class="modal fade" id="deleteIssueModal" tabindex="-1" aria-labelledby="deleteIssueModalLabel" aria-hidden="true">
|
<div class="modal fade" id="deleteIssueModal" tabindex="-1" aria-labelledby="deleteIssueModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<h2><i class="bi bi-folder2-open"></i> Projects</h2>
|
<h2><i class="bi bi-folder2-open"></i> Projects</h2>
|
||||||
</div>
|
</div>
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<a href="{{ url_for('projects.create') }}" class="btn btn-primary">
|
<a href="{{ url_for('projects.create') }}" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> New Project
|
<i class="bi bi-plus-circle"></i> New Project
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<a href="{{ url_for('projects.view', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
|
<a href="{{ url_for('projects.view', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-eye"></i> View
|
<i class="bi bi-eye"></i> View
|
||||||
</a>
|
</a>
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<a href="{{ url_for('projects.edit', project_id=project.id) }}" class="btn btn-sm btn-outline-secondary">
|
<a href="{{ url_for('projects.edit', project_id=project.id) }}" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="bi bi-pencil"></i> Edit
|
<i class="bi bi-pencil"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto d-flex gap-2">
|
<div class="col-auto d-flex gap-2">
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<a href="{{ url_for('projects.edit', project_id=project.id) }}" class="btn btn-outline-secondary">
|
<a href="{{ url_for('projects.edit', project_id=project.id) }}" class="btn btn-outline-secondary">
|
||||||
<i class="bi bi-pencil"></i> Edit
|
<i class="bi bi-pencil"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<h2><i class="bi bi-file-earmark-text"></i> Inspection Templates</h2>
|
<h2><i class="bi bi-file-earmark-text"></i> Inspection Templates</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 text-end">
|
<div class="col-md-6 text-end">
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<a href="{{ url_for('templates.create_template') }}" class="btn btn-primary">
|
<a href="{{ url_for('templates.create_template') }}" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Create Template
|
<i class="bi bi-plus-circle"></i> Create Template
|
||||||
</a>
|
</a>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<i class="bi bi-eye"></i> View
|
<i class="bi bi-eye"></i> View
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<!-- Rename -->
|
<!-- Rename -->
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-outline-secondary"
|
class="btn btn-sm btn-outline-secondary"
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<!-- Edit Template Modal -->
|
<!-- Edit Template Modal -->
|
||||||
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true">
|
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Edit template modal
|
// Edit template modal
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2 flex-shrink-0 mt-1">
|
<div class="d-flex gap-2 flex-shrink-0 mt-1">
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}" class="btn btn-primary btn-sm">
|
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}" class="btn btn-primary btn-sm">
|
||||||
<i class="bi bi-pencil-square"></i> Edit Form
|
<i class="bi bi-pencil-square"></i> Edit Form
|
||||||
</a>
|
</a>
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="bi bi-layout-text-sidebar-reverse"></i>
|
<i class="bi bi-layout-text-sidebar-reverse"></i>
|
||||||
<p>No fields defined yet.</p>
|
<p>No fields defined yet.</p>
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'director'] %}
|
||||||
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}"
|
<a href="{{ url_for('templates.form_editor', template_id=template.id) }}"
|
||||||
class="btn btn-primary btn-sm mt-3">
|
class="btn btn-primary btn-sm mt-3">
|
||||||
<i class="bi bi-plus-circle"></i> Open Form Editor
|
<i class="bi bi-plus-circle"></i> Open Form Editor
|
||||||
|
|||||||
+12
-5
@@ -12,20 +12,27 @@ def admin_required(f):
|
|||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
def supervisor_required(f):
|
def supervisor_required(f):
|
||||||
|
"""Grants access to admin and director roles.
|
||||||
|
|
||||||
|
The decorator is intentionally kept as 'supervisor_required' so that all
|
||||||
|
existing route decorators (@supervisor_required) continue to work without
|
||||||
|
any changes to the route files. The access list now reflects the renamed
|
||||||
|
Director role instead of the retired Supervisor role.
|
||||||
|
"""
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if not current_user.is_authenticated or current_user.role not in ['admin', 'supervisor']:
|
if not current_user.is_authenticated or current_user.role not in ['admin', 'director']:
|
||||||
flash('Supervisor access required.', 'danger')
|
flash('Director access required.', 'danger')
|
||||||
return redirect(url_for('dashboard.index'))
|
return redirect(url_for('dashboard.index'))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
def project_manager_required(f):
|
def project_manager_required(f):
|
||||||
"""Grants access to admin, supervisor, and project_manager roles."""
|
"""Grants access to admin, director, and project_manager roles."""
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if not current_user.is_authenticated or current_user.role not in [
|
if not current_user.is_authenticated or current_user.role not in [
|
||||||
'admin', 'supervisor', 'project_manager'
|
'admin', 'director', 'project_manager'
|
||||||
]:
|
]:
|
||||||
flash('Project Manager access required.', 'danger')
|
flash('Project Manager access required.', 'danger')
|
||||||
return redirect(url_for('dashboard.index'))
|
return redirect(url_for('dashboard.index'))
|
||||||
@@ -35,7 +42,7 @@ def project_manager_required(f):
|
|||||||
def customer_required(f):
|
def customer_required(f):
|
||||||
"""Restricts access to customer-role users only.
|
"""Restricts access to customer-role users only.
|
||||||
|
|
||||||
Internal staff (admin, supervisor, inspector, project_manager) should
|
Internal staff (admin, director, inspector, project_manager) should
|
||||||
never be routed through customer-scoped views — use their own routes.
|
never be routed through customer-scoped views — use their own routes.
|
||||||
"""
|
"""
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
|
|||||||
+4
-3
@@ -51,10 +51,11 @@ class UserForm(FlaskForm):
|
|||||||
password = PasswordField('Password', validators=[Optional(), Length(min=6, max=100)])
|
password = PasswordField('Password', validators=[Optional(), Length(min=6, max=100)])
|
||||||
confirm_password = PasswordField('Confirm Password', validators=[Optional(), EqualTo('password')])
|
confirm_password = PasswordField('Confirm Password', validators=[Optional(), EqualTo('password')])
|
||||||
role = SelectField('Role', choices=[
|
role = SelectField('Role', choices=[
|
||||||
('admin', 'Administrator'),
|
('admin', 'Administrator'),
|
||||||
('supervisor', 'Supervisor'),
|
('director', 'Director'),
|
||||||
('inspector', 'Inspector'),
|
('inspector', 'Inspector'),
|
||||||
('project_manager', 'Project Manager'),
|
('project_manager', 'Project Manager'),
|
||||||
|
# 'customer' is intentionally excluded — customer accounts are managed via /customers
|
||||||
], validators=[DataRequired()])
|
], validators=[DataRequired()])
|
||||||
|
|
||||||
def __init__(self, user=None, *args, **kwargs):
|
def __init__(self, user=None, *args, **kwargs):
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ def notify_by_matrix(
|
|||||||
|
|
||||||
role_to_db = {
|
role_to_db = {
|
||||||
'admin': 'admin',
|
'admin': 'admin',
|
||||||
'supervisor': 'supervisor',
|
'director': 'director',
|
||||||
'inspector': 'inspector',
|
'inspector': 'inspector',
|
||||||
'project_manager': 'project_manager',
|
'project_manager': 'project_manager',
|
||||||
'customer': 'customer',
|
'customer': 'customer',
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""Phase 11: Rename supervisor role to director
|
||||||
|
|
||||||
|
Revision ID: phase11_director_role
|
||||||
|
Revises: phase10_customer_password_setup
|
||||||
|
Create Date: 2026-04-09
|
||||||
|
|
||||||
|
Changes
|
||||||
|
-------
|
||||||
|
1. Adds 'director' to the users.role ENUM.
|
||||||
|
2. Migrates all existing role='supervisor' users to role='director'.
|
||||||
|
3. Removes 'supervisor' from the ENUM once no rows use it.
|
||||||
|
4. Migrates notification_matrix rows keyed role_key='supervisor' → 'director'.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = 'phase11_director_role'
|
||||||
|
down_revision = 'phase10_customer_password_setup'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Step 1 — Expand ENUM to include both values (required before UPDATE)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE users MODIFY COLUMN role "
|
||||||
|
"ENUM('admin','supervisor','director','inspector','project_manager','customer') "
|
||||||
|
"NOT NULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2 — Migrate all supervisor users to director
|
||||||
|
op.execute("UPDATE users SET role = 'director' WHERE role = 'supervisor'")
|
||||||
|
|
||||||
|
# Step 3 — Remove 'supervisor' from the ENUM now that no rows reference it
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE users MODIFY COLUMN role "
|
||||||
|
"ENUM('admin','director','inspector','project_manager','customer') "
|
||||||
|
"NOT NULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 4 — Migrate notification_matrix role_key rows
|
||||||
|
op.execute(
|
||||||
|
"UPDATE notification_matrix SET role_key = 'director' WHERE role_key = 'supervisor'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Step 1 — Expand ENUM to allow supervisor again
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE users MODIFY COLUMN role "
|
||||||
|
"ENUM('admin','supervisor','director','inspector','project_manager','customer') "
|
||||||
|
"NOT NULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2 — Revert director users back to supervisor
|
||||||
|
op.execute("UPDATE users SET role = 'supervisor' WHERE role = 'director'")
|
||||||
|
|
||||||
|
# Step 3 — Remove 'director' from the ENUM
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE users MODIFY COLUMN role "
|
||||||
|
"ENUM('admin','supervisor','inspector','project_manager','customer') "
|
||||||
|
"NOT NULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 4 — Revert notification_matrix role_key rows
|
||||||
|
op.execute(
|
||||||
|
"UPDATE notification_matrix SET role_key = 'supervisor' WHERE role_key = 'director'"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user