Phase 2: initiate
This commit is contained in:
@@ -6,6 +6,12 @@ from flask_mail import Mail
|
|||||||
from config import config
|
from config import config
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import app
|
||||||
|
from app.routes import auth, dashboard, inspections, templates, reports, facilities
|
||||||
|
|
||||||
|
# Add to blueprint registration
|
||||||
|
app.register_blueprint(facilities.bp)
|
||||||
|
|
||||||
# Initialize extensions
|
# Initialize extensions
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
|
|||||||
+80
-9
@@ -1,30 +1,101 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
from flask_login import login_user, logout_user, login_required
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
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
|
||||||
|
from app.utils.decorators import admin_required
|
||||||
|
|
||||||
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||||
|
|
||||||
@bp.route('/login', methods=['GET', 'POST'])
|
@bp.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
if request.method == 'POST':
|
if current_user.is_authenticated:
|
||||||
username = request.form.get('username')
|
return redirect(url_for('dashboard.index'))
|
||||||
password = request.form.get('password')
|
|
||||||
|
|
||||||
user = User.query.filter_by(username=username).first()
|
form = LoginForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User.query.filter_by(username=form.username.data).first()
|
||||||
|
|
||||||
if user and user.check_password(password):
|
if user and user.check_password(form.password.data):
|
||||||
login_user(user)
|
login_user(user)
|
||||||
next_page = request.args.get('next')
|
next_page = request.args.get('next')
|
||||||
|
flash(f'Welcome back, {user.username}!', 'success')
|
||||||
return redirect(next_page or url_for('dashboard.index'))
|
return redirect(next_page or url_for('dashboard.index'))
|
||||||
else:
|
else:
|
||||||
flash('Invalid username or password', 'danger')
|
flash('Invalid credentials. Please try again.', 'danger')
|
||||||
|
|
||||||
return render_template('login.html')
|
return render_template('auth/login.html', form=form)
|
||||||
|
|
||||||
@bp.route('/logout')
|
@bp.route('/logout')
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
flash('You have been logged out successfully', 'success')
|
flash('Successfully logged out.', 'success')
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
@bp.route('/users')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def list_users():
|
||||||
|
users = User.query.order_by(User.created_at.desc()).all()
|
||||||
|
return render_template('auth/users.html', users=users)
|
||||||
|
|
||||||
|
@bp.route('/users/new', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def create_user():
|
||||||
|
form = UserForm()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User(
|
||||||
|
username=form.username.data,
|
||||||
|
email=form.email.data,
|
||||||
|
role=form.role.data
|
||||||
|
)
|
||||||
|
user.set_password(form.password.data)
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'User {user.username} created successfully.', 'success')
|
||||||
|
return redirect(url_for('auth.list_users'))
|
||||||
|
|
||||||
|
return render_template('auth/user_form.html', form=form, title='Create User')
|
||||||
|
|
||||||
|
@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def edit_user(user_id):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
form = UserForm(user=user, obj=user)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user.username = form.username.data
|
||||||
|
user.email = form.email.data
|
||||||
|
user.role = form.role.data
|
||||||
|
|
||||||
|
if form.password.data:
|
||||||
|
user.set_password(form.password.data)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f'User {user.username} updated successfully.', 'success')
|
||||||
|
return redirect(url_for('auth.list_users'))
|
||||||
|
|
||||||
|
return render_template('auth/user_form.html', form=form, user=user, title='Edit User')
|
||||||
|
|
||||||
|
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def delete_user(user_id):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
|
if user.id == current_user.id:
|
||||||
|
flash('Cannot delete your own account.', 'danger')
|
||||||
|
return redirect(url_for('auth.list_users'))
|
||||||
|
|
||||||
|
username = user.username
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'User {username} deleted successfully.', 'success')
|
||||||
|
return redirect(url_for('auth.list_users'))
|
||||||
+83
-1
@@ -1,5 +1,11 @@
|
|||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
from app.models.inspection import Inspection, InspectionTemplate
|
||||||
|
from app.models.facility import Facility
|
||||||
|
from app.models.issue import Issue
|
||||||
|
from app.models.user import User
|
||||||
|
from sqlalchemy import func
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
bp = Blueprint('dashboard', __name__)
|
bp = Blueprint('dashboard', __name__)
|
||||||
|
|
||||||
@@ -7,4 +13,80 @@ bp = Blueprint('dashboard', __name__)
|
|||||||
@bp.route('/dashboard')
|
@bp.route('/dashboard')
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
return render_template('dashboard.html', user=current_user)
|
# Get today's date range
|
||||||
|
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
today_end = today_start + timedelta(days=1)
|
||||||
|
|
||||||
|
# Statistics for today
|
||||||
|
if current_user.role == 'inspector':
|
||||||
|
today_inspections = Inspection.query.filter(
|
||||||
|
Inspection.inspector_id == current_user.id,
|
||||||
|
Inspection.inspection_date >= today_start,
|
||||||
|
Inspection.inspection_date < today_end
|
||||||
|
).count()
|
||||||
|
|
||||||
|
completed_today = Inspection.query.filter(
|
||||||
|
Inspection.inspector_id == current_user.id,
|
||||||
|
Inspection.status == 'completed',
|
||||||
|
Inspection.inspection_date >= today_start,
|
||||||
|
Inspection.inspection_date < today_end
|
||||||
|
).count()
|
||||||
|
|
||||||
|
open_issues = Issue.query.join(Inspection).filter(
|
||||||
|
Inspection.inspector_id == current_user.id,
|
||||||
|
Issue.status.in_(['open', 'in_progress'])
|
||||||
|
).count()
|
||||||
|
|
||||||
|
else:
|
||||||
|
today_inspections = Inspection.query.filter(
|
||||||
|
Inspection.inspection_date >= today_start,
|
||||||
|
Inspection.inspection_date < today_end
|
||||||
|
).count()
|
||||||
|
|
||||||
|
completed_today = Inspection.query.filter(
|
||||||
|
Inspection.status == 'completed',
|
||||||
|
Inspection.inspection_date >= today_start,
|
||||||
|
Inspection.inspection_date < today_end
|
||||||
|
).count()
|
||||||
|
|
||||||
|
open_issues = Issue.query.filter(
|
||||||
|
Issue.status.in_(['open', 'in_progress'])
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Calculate average score (last 30 days)
|
||||||
|
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
||||||
|
avg_score_query = Inspection.query.filter(
|
||||||
|
Inspection.status == 'completed',
|
||||||
|
Inspection.overall_score.isnot(None),
|
||||||
|
Inspection.inspection_date >= thirty_days_ago
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_user.role == 'inspector':
|
||||||
|
avg_score_query = avg_score_query.filter(Inspection.inspector_id == current_user.id)
|
||||||
|
|
||||||
|
avg_score = db.session.query(func.avg(Inspection.overall_score)).filter(
|
||||||
|
Inspection.id.in_([i.id for i in avg_score_query.all()])
|
||||||
|
).scalar()
|
||||||
|
|
||||||
|
# Recent inspections
|
||||||
|
recent_inspections_query = Inspection.query.order_by(Inspection.inspection_date.desc()).limit(5)
|
||||||
|
if current_user.role == 'inspector':
|
||||||
|
recent_inspections_query = recent_inspections_query.filter(Inspection.inspector_id == current_user.id)
|
||||||
|
|
||||||
|
recent_inspections = recent_inspections_query.all()
|
||||||
|
|
||||||
|
# System statistics (admin/supervisor only)
|
||||||
|
total_facilities = Facility.query.filter_by(active=True).count() if current_user.role in ['admin', 'supervisor'] else 0
|
||||||
|
total_templates = InspectionTemplate.query.count() if current_user.role in ['admin', 'supervisor'] else 0
|
||||||
|
total_users = User.query.count() if current_user.role == 'admin' else 0
|
||||||
|
|
||||||
|
return render_template('dashboard.html',
|
||||||
|
today_inspections=today_inspections,
|
||||||
|
completed_today=completed_today,
|
||||||
|
open_issues=open_issues,
|
||||||
|
avg_score=round(avg_score, 2) if avg_score else None,
|
||||||
|
recent_inspections=recent_inspections,
|
||||||
|
total_facilities=total_facilities,
|
||||||
|
total_templates=total_templates,
|
||||||
|
total_users=total_users
|
||||||
|
)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
|
from flask_login import login_required
|
||||||
|
from app import db
|
||||||
|
from app.models.facility import Facility, Area
|
||||||
|
from app.utils.forms import FacilityForm, AreaForm
|
||||||
|
from app.utils.decorators import supervisor_required
|
||||||
|
|
||||||
|
bp = Blueprint('facilities', __name__, url_prefix='/facilities')
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
@login_required
|
||||||
|
def list_facilities():
|
||||||
|
facilities = Facility.query.order_by(Facility.name).all()
|
||||||
|
return render_template('facilities/list.html', facilities=facilities)
|
||||||
|
|
||||||
|
@bp.route('/new', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def create_facility():
|
||||||
|
form = FacilityForm()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
facility = Facility(
|
||||||
|
name=form.name.data,
|
||||||
|
address=form.address.data,
|
||||||
|
contact_person=form.contact_person.data,
|
||||||
|
contact_phone=form.contact_phone.data,
|
||||||
|
active=form.active.data
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(facility)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'Facility "{facility.name}" created successfully.', 'success')
|
||||||
|
return redirect(url_for('facilities.view_facility', facility_id=facility.id))
|
||||||
|
|
||||||
|
return render_template('facilities/form.html', form=form, title='Create Facility')
|
||||||
|
|
||||||
|
@bp.route('/<int:facility_id>')
|
||||||
|
@login_required
|
||||||
|
def view_facility(facility_id):
|
||||||
|
facility = Facility.query.get_or_404(facility_id)
|
||||||
|
areas = facility.areas.order_by(Area.name).all()
|
||||||
|
return render_template('facilities/view.html', facility=facility, areas=areas)
|
||||||
|
|
||||||
|
@bp.route('/<int:facility_id>/edit', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def edit_facility(facility_id):
|
||||||
|
facility = Facility.query.get_or_404(facility_id)
|
||||||
|
form = FacilityForm(obj=facility)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
facility.name = form.name.data
|
||||||
|
facility.address = form.address.data
|
||||||
|
facility.contact_person = form.contact_person.data
|
||||||
|
facility.contact_phone = form.contact_phone.data
|
||||||
|
facility.active = form.active.data
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f'Facility "{facility.name}" updated successfully.', 'success')
|
||||||
|
return redirect(url_for('facilities.view_facility', facility_id=facility.id))
|
||||||
|
|
||||||
|
return render_template('facilities/form.html', form=form, facility=facility, title='Edit Facility')
|
||||||
|
|
||||||
|
@bp.route('/<int:facility_id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def delete_facility(facility_id):
|
||||||
|
facility = Facility.query.get_or_404(facility_id)
|
||||||
|
|
||||||
|
# Check if facility has inspections
|
||||||
|
if facility.inspections.count() > 0:
|
||||||
|
flash('Cannot delete facility with existing inspections.', 'danger')
|
||||||
|
return redirect(url_for('facilities.view_facility', facility_id=facility.id))
|
||||||
|
|
||||||
|
facility_name = facility.name
|
||||||
|
db.session.delete(facility)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'Facility "{facility_name}" deleted successfully.', 'success')
|
||||||
|
return redirect(url_for('facilities.list_facilities'))
|
||||||
|
|
||||||
|
# Area Management Routes
|
||||||
|
|
||||||
|
@bp.route('/<int:facility_id>/areas/new', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def create_area(facility_id):
|
||||||
|
facility = Facility.query.get_or_404(facility_id)
|
||||||
|
form = AreaForm()
|
||||||
|
form.facility_id.choices = [(facility.id, facility.name)]
|
||||||
|
form.facility_id.data = facility.id
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
area = Area(
|
||||||
|
name=form.name.data,
|
||||||
|
area_type=form.area_type.data,
|
||||||
|
facility_id=facility.id
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(area)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'Area "{area.name}" created successfully.', 'success')
|
||||||
|
return redirect(url_for('facilities.view_facility', facility_id=facility.id))
|
||||||
|
|
||||||
|
return render_template('facilities/area_form.html', form=form, facility=facility, title='Create Area')
|
||||||
|
|
||||||
|
@bp.route('/areas/<int:area_id>/edit', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def edit_area(area_id):
|
||||||
|
area = Area.query.get_or_404(area_id)
|
||||||
|
form = AreaForm(obj=area)
|
||||||
|
|
||||||
|
# Populate facility choices
|
||||||
|
facilities = Facility.query.filter_by(active=True).order_by(Facility.name).all()
|
||||||
|
form.facility_id.choices = [(f.id, f.name) for f in facilities]
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
area.name = form.name.data
|
||||||
|
area.area_type = form.area_type.data
|
||||||
|
area.facility_id = form.facility_id.data
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f'Area "{area.name}" updated successfully.', 'success')
|
||||||
|
return redirect(url_for('facilities.view_facility', facility_id=area.facility_id))
|
||||||
|
|
||||||
|
return render_template('facilities/area_form.html', form=form, area=area, facility=area.facility, title='Edit Area')
|
||||||
|
|
||||||
|
@bp.route('/areas/<int:area_id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def delete_area(area_id):
|
||||||
|
area = Area.query.get_or_404(area_id)
|
||||||
|
facility_id = area.facility_id
|
||||||
|
|
||||||
|
# Check if area has inspections
|
||||||
|
if area.inspections.count() > 0:
|
||||||
|
flash('Cannot delete area with existing inspections.', 'danger')
|
||||||
|
return redirect(url_for('facilities.view_facility', facility_id=facility_id))
|
||||||
|
|
||||||
|
area_name = area.name
|
||||||
|
db.session.delete(area)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'Area "{area_name}" deleted successfully.', 'success')
|
||||||
|
return redirect(url_for('facilities.view_facility', facility_id=facility_id))
|
||||||
+165
-2
@@ -1,7 +1,170 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from app import db
|
||||||
|
from app.models.inspection import InspectionTemplate, ChecklistItem
|
||||||
|
from app.utils.forms import InspectionTemplateForm, ChecklistItemForm
|
||||||
|
from app.utils.decorators import supervisor_required
|
||||||
|
|
||||||
bp = Blueprint('templates', __name__, url_prefix='/templates')
|
bp = Blueprint('templates', __name__, url_prefix='/templates')
|
||||||
|
|
||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
return "Templates module - Coming in Phase 2"
|
templates = InspectionTemplate.query.order_by(InspectionTemplate.name).all()
|
||||||
|
return render_template('templates/list.html', templates=templates)
|
||||||
|
|
||||||
|
@bp.route('/new', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def create_template():
|
||||||
|
form = InspectionTemplateForm()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
template = InspectionTemplate(
|
||||||
|
name=form.name.data,
|
||||||
|
description=form.description.data,
|
||||||
|
frequency=form.frequency.data,
|
||||||
|
created_by=current_user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(template)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'Template "{template.name}" created successfully.', 'success')
|
||||||
|
return redirect(url_for('templates.edit_template', template_id=template.id))
|
||||||
|
|
||||||
|
return render_template('templates/form.html', form=form, title='Create Inspection Template')
|
||||||
|
|
||||||
|
@bp.route('/<int:template_id>')
|
||||||
|
@login_required
|
||||||
|
def view_template(template_id):
|
||||||
|
template = InspectionTemplate.query.get_or_404(template_id)
|
||||||
|
checklist_items = template.checklist_items.order_by(ChecklistItem.display_order, ChecklistItem.category).all()
|
||||||
|
|
||||||
|
# Group items by category
|
||||||
|
items_by_category = {}
|
||||||
|
for item in checklist_items:
|
||||||
|
category = item.category or 'General'
|
||||||
|
if category not in items_by_category:
|
||||||
|
items_by_category[category] = []
|
||||||
|
items_by_category[category].append(item)
|
||||||
|
|
||||||
|
return render_template('templates/view.html', template=template, items_by_category=items_by_category)
|
||||||
|
|
||||||
|
@bp.route('/<int:template_id>/edit', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def edit_template(template_id):
|
||||||
|
template = InspectionTemplate.query.get_or_404(template_id)
|
||||||
|
form = InspectionTemplateForm(obj=template)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
template.name = form.name.data
|
||||||
|
template.description = form.description.data
|
||||||
|
template.frequency = form.frequency.data
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f'Template "{template.name}" updated successfully.', 'success')
|
||||||
|
return redirect(url_for('templates.view_template', template_id=template.id))
|
||||||
|
|
||||||
|
checklist_items = template.checklist_items.order_by(ChecklistItem.display_order, ChecklistItem.category).all()
|
||||||
|
|
||||||
|
return render_template('templates/edit.html', form=form, template=template, checklist_items=checklist_items)
|
||||||
|
|
||||||
|
@bp.route('/<int:template_id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def delete_template(template_id):
|
||||||
|
template = InspectionTemplate.query.get_or_404(template_id)
|
||||||
|
|
||||||
|
# Check if template has inspections
|
||||||
|
if template.inspections.count() > 0:
|
||||||
|
flash('Cannot delete template with existing inspections.', 'danger')
|
||||||
|
return redirect(url_for('templates.view_template', template_id=template.id))
|
||||||
|
|
||||||
|
template_name = template.name
|
||||||
|
db.session.delete(template)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'Template "{template_name}" deleted successfully.', 'success')
|
||||||
|
return redirect(url_for('templates.index'))
|
||||||
|
|
||||||
|
# Checklist Item Management
|
||||||
|
|
||||||
|
@bp.route('/<int:template_id>/items/new', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def create_checklist_item(template_id):
|
||||||
|
template = InspectionTemplate.query.get_or_404(template_id)
|
||||||
|
form = ChecklistItemForm()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
# Get the highest display order
|
||||||
|
max_order = db.session.query(db.func.max(ChecklistItem.display_order))\
|
||||||
|
.filter_by(template_id=template.id).scalar() or 0
|
||||||
|
|
||||||
|
item = ChecklistItem(
|
||||||
|
template_id=template.id,
|
||||||
|
category=form.category.data,
|
||||||
|
item_description=form.item_description.data,
|
||||||
|
scoring_type=form.scoring_type.data,
|
||||||
|
weight=form.weight.data,
|
||||||
|
requires_photo=form.requires_photo.data,
|
||||||
|
display_order=max_order + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash('Checklist item added successfully.', 'success')
|
||||||
|
return redirect(url_for('templates.edit_template', template_id=template.id))
|
||||||
|
|
||||||
|
return render_template('templates/item_form.html', form=form, template=template, title='Add Checklist Item')
|
||||||
|
|
||||||
|
@bp.route('/items/<int:item_id>/edit', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def edit_checklist_item(item_id):
|
||||||
|
item = ChecklistItem.query.get_or_404(item_id)
|
||||||
|
form = ChecklistItemForm(obj=item)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
item.category = form.category.data
|
||||||
|
item.item_description = form.item_description.data
|
||||||
|
item.scoring_type = form.scoring_type.data
|
||||||
|
item.weight = form.weight.data
|
||||||
|
item.requires_photo = form.requires_photo.data
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash('Checklist item updated successfully.', 'success')
|
||||||
|
return redirect(url_for('templates.edit_template', template_id=item.template_id))
|
||||||
|
|
||||||
|
return render_template('templates/item_form.html', form=form, item=item, template=item.template, title='Edit Checklist Item')
|
||||||
|
|
||||||
|
@bp.route('/items/<int:item_id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def delete_checklist_item(item_id):
|
||||||
|
item = ChecklistItem.query.get_or_404(item_id)
|
||||||
|
template_id = item.template_id
|
||||||
|
|
||||||
|
db.session.delete(item)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash('Checklist item deleted successfully.', 'success')
|
||||||
|
return redirect(url_for('templates.edit_template', template_id=template_id))
|
||||||
|
|
||||||
|
@bp.route('/<int:template_id>/items/reorder', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def reorder_items(template_id):
|
||||||
|
template = InspectionTemplate.query.get_or_404(template_id)
|
||||||
|
item_order = request.json.get('item_order', [])
|
||||||
|
|
||||||
|
for index, item_id in enumerate(item_order):
|
||||||
|
item = ChecklistItem.query.get(item_id)
|
||||||
|
if item and item.template_id == template.id:
|
||||||
|
item.display_order = index
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True})
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h4 class="mb-0">{{ title }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.username.label(class="form-label") }}
|
||||||
|
{{ form.username(class="form-control") }}
|
||||||
|
{% if form.username.errors %}
|
||||||
|
<div class="text-danger small mt-1">
|
||||||
|
{% for error in form.username.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.email.label(class="form-label") }}
|
||||||
|
{{ form.email(class="form-control") }}
|
||||||
|
{% if form.email.errors %}
|
||||||
|
<div class="text-danger small mt-1">
|
||||||
|
{% for error in form.email.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.password.label(class="form-label") }}
|
||||||
|
{{ form.password(class="form-control", placeholder="Leave blank to keep current" if user else "") }}
|
||||||
|
{% if form.password.errors %}
|
||||||
|
<div class="text-danger small mt-1">
|
||||||
|
{% for error in form.password.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.confirm_password.label(class="form-label") }}
|
||||||
|
{{ form.confirm_password(class="form-control") }}
|
||||||
|
{% if form.confirm_password.errors %}
|
||||||
|
<div class="text-danger small mt-1">
|
||||||
|
{% for error in form.confirm_password.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
{{ form.role.label(class="form-label") }}
|
||||||
|
{{ form.role(class="form-select") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i> Save User
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('auth.list_users') }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}User Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2><i class="bi bi-people-fill"></i> User Management</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-end">
|
||||||
|
<a href="{{ url_for('auth.create_user') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Add User
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th width="150">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ user.username }}</strong></td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{% if user.role == 'admin' %}danger{% elif user.role == 'supervisor' %}warning{% else %}info{% endif %}">
|
||||||
|
{{ user.role|title }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('auth.edit_user', user_id=user.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
{% if user.id != current_user.id %}
|
||||||
|
<form method="POST" action="{{ url_for('auth.delete_user', user_id=user.id) }}" class="d-inline" onsubmit="return confirm('Delete this user?');">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -24,14 +24,22 @@
|
|||||||
<a class="nav-link" href="{{ url_for('dashboard.index') }}">Dashboard</a>
|
<a class="nav-link" href="{{ url_for('dashboard.index') }}">Dashboard</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('inspections.index') }}">Inspections</a>
|
<a class="nav-link" href="{{ url_for('facilities.list_facilities') }}">Facilities</a>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('inspections.index') }}">Inspections</a>
|
||||||
|
</li>
|
||||||
<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 == 'admin' %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('auth.list_users') }}">Users</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|||||||
+145
-20
@@ -1,45 +1,170 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Dashboard - Janitorial QC{% endblock %}
|
{% block title %}Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h2>Welcome, {{ current_user.username }}!</h2>
|
<h2>Welcome, {{ current_user.username }}!</h2>
|
||||||
<p class="text-muted">Role: {{ current_user.role|title }}</p>
|
<p class="text-muted">
|
||||||
|
<span class="badge bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'supervisor' %}warning{% else %}info{% endif %}">
|
||||||
|
{{ current_user.role|title }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3 mb-4">
|
||||||
<div class="card text-white bg-primary mb-3">
|
<div class="card text-white bg-primary h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title"><i class="bi bi-clipboard-data"></i> Today's Inspections</h5>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<p class="card-text display-6">0</p>
|
<div>
|
||||||
|
<h6 class="card-title text-white-50">Today's Inspections</h6>
|
||||||
|
<h2 class="mb-0">{{ today_inspections }}</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-clipboard-data" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card text-white bg-success mb-3">
|
<div class="col-md-3 mb-4">
|
||||||
|
<div class="card text-white bg-success h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title"><i class="bi bi-check-circle"></i> Completed</h5>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<p class="card-text display-6">0</p>
|
<div>
|
||||||
|
<h6 class="card-title text-white-50">Completed Today</h6>
|
||||||
|
<h2 class="mb-0">{{ completed_today }}</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-check-circle" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card text-white bg-warning mb-3">
|
<div class="col-md-3 mb-4">
|
||||||
|
<div class="card text-white bg-warning h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title"><i class="bi bi-exclamation-triangle"></i> Issues Open</h5>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<p class="card-text display-6">0</p>
|
<div>
|
||||||
|
<h6 class="card-title text-white-50">Open Issues</h6>
|
||||||
|
<h2 class="mb-0">{{ open_issues }}</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-exclamation-triangle" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card text-white bg-info mb-3">
|
<div class="col-md-3 mb-4">
|
||||||
|
<div class="card text-white bg-info h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title"><i class="bi bi-graph-up"></i> Avg Score</h5>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<p class="card-text display-6">--</p>
|
<div>
|
||||||
|
<h6 class="card-title text-white-50">Avg Score (30d)</h6>
|
||||||
|
<h2 class="mb-0">{{ avg_score if avg_score else '--' }}%</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-graph-up" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-building text-primary" style="font-size: 2.5rem;"></i>
|
||||||
|
<h3 class="mt-2">{{ total_facilities }}</h3>
|
||||||
|
<p class="text-muted mb-0">Active Facilities</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-file-earmark-text text-success" style="font-size: 2.5rem;"></i>
|
||||||
|
<h3 class="mt-2">{{ total_templates }}</h3>
|
||||||
|
<p class="text-muted mb-0">Templates</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if current_user.role == 'admin' %}
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-people text-warning" style="font-size: 2.5rem;"></i>
|
||||||
|
<h3 class="mt-2">{{ total_users }}</h3>
|
||||||
|
<p class="text-muted mb-0">Users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Recent Activity</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if recent_inspections %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Facility</th>
|
||||||
|
<th>Area</th>
|
||||||
|
<th>Inspector</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for inspection in recent_inspections %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ inspection.inspection_date.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
|
<td>{{ inspection.facility.name }}</td>
|
||||||
|
<td>{{ inspection.area.name if inspection.area else 'N/A' }}</td>
|
||||||
|
<td>{{ inspection.inspector.username }}</td>
|
||||||
|
<td>
|
||||||
|
{% if inspection.overall_score %}
|
||||||
|
<span class="badge bg-{% if inspection.overall_score >= 90 %}success{% elif inspection.overall_score >= 70 %}warning{% else %}danger{% endif %}">
|
||||||
|
{{ inspection.overall_score }}%
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">--</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{% if inspection.status == 'completed' %}success{% elif inspection.status == 'flagged' %}danger{% else %}secondary{% endif %}">
|
||||||
|
{{ inspection.status|title }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<i class="bi bi-info-circle"></i> No recent inspections to display.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h4 class="mb-0">{{ title }} - {{ facility.name }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.name.label(class="form-label") }}
|
||||||
|
{{ form.name(class="form-control") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
{{ form.area_type.label(class="form-label") }}
|
||||||
|
{{ form.area_type(class="form-select") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i> Save Area
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('facilities.view_facility', facility_id=facility.id) }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h4 class="mb-0">{{ title }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.name.label(class="form-label") }}
|
||||||
|
{{ form.name(class="form-control") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.address.label(class="form-label") }}
|
||||||
|
{{ form.address(class="form-control", rows=3) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.contact_person.label(class="form-label") }}
|
||||||
|
{{ form.contact_person(class="form-control") }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.contact_phone.label(class="form-label") }}
|
||||||
|
{{ form.contact_phone(class="form-control") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.active(class="form-check-input") }}
|
||||||
|
{{ form.active.label(class="form-check-label") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i> Save Facility
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('facilities.list_facilities') }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Facilities{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2><i class="bi bi-building"></i> Facilities</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-end">
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<a href="{{ url_for('facilities.create_facility') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Add Facility
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for facility in facilities %}
|
||||||
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<a href="{{ url_for('facilities.view_facility', facility_id=facility.id) }}" class="text-decoration-none">
|
||||||
|
{{ facility.name }}
|
||||||
|
</a>
|
||||||
|
{% if not facility.active %}
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
{% if facility.address %}
|
||||||
|
<p class="card-text text-muted small">
|
||||||
|
<i class="bi bi-geo-alt"></i> {{ facility.address }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-diagram-3"></i> {{ facility.areas.count() }} areas
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent">
|
||||||
|
<a href="{{ url_for('facilities.view_facility', facility_id=facility.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-eye"></i> View Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> No facilities configured yet.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ facility.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h2><i class="bi bi-building"></i> {{ facility.name }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<a href="{{ url_for('facilities.edit_facility', facility_id=facility.id) }}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i> Edit
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('facilities.create_area', facility_id=facility.id) }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Add Area
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0">Facility Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-sm table-borderless">
|
||||||
|
<tr>
|
||||||
|
<th width="40%">Address:</th>
|
||||||
|
<td>{{ facility.address or 'N/A' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Contact Person:</th>
|
||||||
|
<td>{{ facility.contact_person or 'N/A' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Contact Phone:</th>
|
||||||
|
<td>{{ facility.contact_phone or 'N/A' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status:</th>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ 'success' if facility.active else 'secondary' }}">
|
||||||
|
{{ 'Active' if facility.active else 'Inactive' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0">Statistics</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-6">
|
||||||
|
<h3 class="text-primary">{{ areas|length }}</h3>
|
||||||
|
<small class="text-muted">Areas</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<h3 class="text-info">{{ facility.inspections.count() }}</h3>
|
||||||
|
<small class="text-muted">Inspections</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-diagram-3"></i> Areas</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if areas %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Area Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Inspections</th>
|
||||||
|
<th width="150">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for area in areas %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ area.name }}</strong></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ area.area_type|title if area.area_type else 'N/A' }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ area.inspections.count() }}</td>
|
||||||
|
<td>
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<a href="{{ url_for('facilities.edit_area', area_id=area.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ url_for('facilities.delete_area', area_id=area.id) }}" class="d-inline" onsubmit="return confirm('Delete this area?');">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<i class="bi bi-info-circle"></i> No areas defined for this facility yet.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
+31
-11
@@ -4,24 +4,44 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center mt-5">
|
<div class="row justify-content-center mt-5">
|
||||||
<div class="col-md-4">
|
<div class="col-md-5 col-lg-4">
|
||||||
<div class="card shadow">
|
<div class="card shadow-lg">
|
||||||
<div class="card-header bg-primary text-white text-center">
|
<div class="card-header bg-primary text-white text-center py-4">
|
||||||
<h4><i class="bi bi-clipboard-check"></i> Janitorial QC System</h4>
|
<h3><i class="bi bi-clipboard-check-fill"></i> Janitorial QC</h3>
|
||||||
|
<p class="mb-0">Quality Control System</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body p-4">
|
||||||
<form method="POST" action="{{ url_for('auth.login') }}">
|
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Username</label>
|
{{ form.username.label(class="form-label") }}
|
||||||
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
{{ form.username(class="form-control form-control-lg", placeholder="Enter username") }}
|
||||||
|
{% if form.username.errors %}
|
||||||
|
<div class="text-danger small mt-1">
|
||||||
|
{% for error in form.username.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Password</label>
|
<div class="mb-4">
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
{{ form.password.label(class="form-label") }}
|
||||||
|
{{ form.password(class="form-control form-control-lg", placeholder="Enter password") }}
|
||||||
|
{% if form.password.errors %}
|
||||||
|
<div class="text-danger small mt-1">
|
||||||
|
{% for error in form.password.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg w-100">
|
||||||
|
<i class="bi bi-box-arrow-in-right"></i> Login
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-footer text-center text-muted small">
|
||||||
|
© 2025 Janitorial QC System
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Edit Template - {{ template.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.checklist-item { cursor: move; }
|
||||||
|
.checklist-item:hover { background-color: #f8f9fa; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h2><i class="bi bi-file-earmark-text"></i> {{ template.name }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<a href="{{ url_for('templates.create_checklist_item', template_id=template.id) }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Add Item
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0">Template Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.name.label(class="form-label") }}
|
||||||
|
{{ form.name(class="form-control") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.description.label(class="form-label") }}
|
||||||
|
{{ form.description(class="form-control", rows=3) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.frequency.label(class="form-label") }}
|
||||||
|
{{ form.frequency(class="form-select") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
<i class="bi bi-save"></i> Update Template
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-check2-square"></i> Checklist Items ({{ checklist_items|length }})</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if checklist_items %}
|
||||||
|
<div id="checklist-items" class="list-group">
|
||||||
|
{% for item in checklist_items %}
|
||||||
|
<div class="list-group-item checklist-item" data-item-id="{{ item.id }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-center mb-1">
|
||||||
|
<i class="bi bi-grip-vertical text-muted me-2"></i>
|
||||||
|
<span class="badge bg-secondary me-2">{{ item.category }}</span>
|
||||||
|
<span class="badge bg-info">{{ item.scoring_type.replace('_', ' ')|title }}</span>
|
||||||
|
{% if item.requires_photo %}
|
||||||
|
<span class="badge bg-warning ms-2"><i class="bi bi-camera"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p class="mb-1">{{ item.item_description }}</p>
|
||||||
|
<small class="text-muted">Weight: {{ item.weight }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<a href="{{ url_for('templates.edit_checklist_item', item_id=item.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ url_for('templates.delete_checklist_item', item_id=item.id) }}" class="d-inline" onsubmit="return confirm('Delete this item?');">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<i class="bi bi-info-circle"></i> No checklist items yet. Click "Add Item" to get started.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const el = document.getElementById('checklist-items');
|
||||||
|
if (el) {
|
||||||
|
new Sortable(el, {
|
||||||
|
animation: 150,
|
||||||
|
handle: '.bi-grip-vertical',
|
||||||
|
onEnd: function(evt) {
|
||||||
|
const itemOrder = [];
|
||||||
|
el.querySelectorAll('.checklist-item').forEach(item => {
|
||||||
|
itemOrder.push(item.getAttribute('data-item-id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch('{{ url_for("templates.reorder_items", template_id=template.id) }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ item_order: itemOrder })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 offset-md-3">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h4 class="mb-0">{{ title }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.name.label(class="form-label") }}
|
||||||
|
{{ form.name(class="form-control") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.description.label(class="form-label") }}
|
||||||
|
{{ form.description(class="form-control", rows=3) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
{{ form.frequency.label(class="form-label") }}
|
||||||
|
{{ form.frequency(class="form-select") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i> Save Template
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('templates.index') }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h4 class="mb-0">{{ title }}</h4>
|
||||||
|
<small>Template: {{ template.name }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.category.label(class="form-label") }}
|
||||||
|
{{ form.category(class="form-control", placeholder="e.g., Restrooms, Floors, etc.") }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.scoring_type.label(class="form-label") }}
|
||||||
|
{{ form.scoring_type(class="form-select") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.item_description.label(class="form-label") }}
|
||||||
|
{{ form.item_description(class="form-control", rows=3) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.weight.label(class="form-label") }}
|
||||||
|
{{ form.weight(class="form-control") }}
|
||||||
|
<small class="text-muted">1.0 = standard weight</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label"> </label>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.requires_photo(class="form-check-input") }}
|
||||||
|
{{ form.requires_photo.label(class="form-check-label") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i> Save Item
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('templates.edit_template', template_id=template.id) }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Inspection Templates{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2><i class="bi bi-file-earmark-text"></i> Inspection Templates</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-end">
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<a href="{{ url_for('templates.create_template') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Create Template
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for template in templates %}
|
||||||
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<a href="{{ url_for('templates.view_template', template_id=template.id) }}" class="text-decoration-none">
|
||||||
|
{{ template.name }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<p class="card-text text-muted small">{{ template.description or 'No description' }}</p>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<span class="badge bg-info">{{ template.frequency|title }}</span>
|
||||||
|
<small class="text-muted ms-2">
|
||||||
|
<i class="bi bi-check2-square"></i> {{ template.checklist_items.count() }} items
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent">
|
||||||
|
<a href="{{ url_for('templates.view_template', template_id=template.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-eye"></i> View Template
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> No templates created yet.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from flask import flash, redirect, url_for
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
def admin_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not current_user.is_authenticated or current_user.role != 'admin':
|
||||||
|
flash('Administrator access required.', 'danger')
|
||||||
|
return redirect(url_for('dashboard.index'))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
def supervisor_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not current_user.is_authenticated or current_user.role not in ['admin', 'supervisor']:
|
||||||
|
flash('Supervisor access required.', 'danger')
|
||||||
|
return redirect(url_for('dashboard.index'))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, PasswordField, SelectField, TextAreaField, DecimalField, BooleanField, IntegerField
|
||||||
|
from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional, NumberRange, ValidationError
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
|
||||||
|
password = PasswordField('Password', validators=[DataRequired()])
|
||||||
|
|
||||||
|
class UserForm(FlaskForm):
|
||||||
|
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
|
||||||
|
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
||||||
|
password = PasswordField('Password', validators=[Length(min=6, max=100)])
|
||||||
|
confirm_password = PasswordField('Confirm Password', validators=[EqualTo('password')])
|
||||||
|
role = SelectField('Role', choices=[
|
||||||
|
('admin', 'Administrator'),
|
||||||
|
('supervisor', 'Supervisor'),
|
||||||
|
('inspector', 'Inspector')
|
||||||
|
], validators=[DataRequired()])
|
||||||
|
|
||||||
|
def __init__(self, user=None, *args, **kwargs):
|
||||||
|
super(UserForm, self).__init__(*args, **kwargs)
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
def validate_username(self, field):
|
||||||
|
if self.user:
|
||||||
|
# Editing existing user
|
||||||
|
if field.data != self.user.username:
|
||||||
|
if User.query.filter_by(username=field.data).first():
|
||||||
|
raise ValidationError('Username already exists.')
|
||||||
|
else:
|
||||||
|
# Creating new user
|
||||||
|
if User.query.filter_by(username=field.data).first():
|
||||||
|
raise ValidationError('Username already exists.')
|
||||||
|
|
||||||
|
def validate_email(self, field):
|
||||||
|
if self.user:
|
||||||
|
if field.data != self.user.email:
|
||||||
|
if User.query.filter_by(email=field.data).first():
|
||||||
|
raise ValidationError('Email already registered.')
|
||||||
|
else:
|
||||||
|
if User.query.filter_by(email=field.data).first():
|
||||||
|
raise ValidationError('Email already registered.')
|
||||||
|
|
||||||
|
class FacilityForm(FlaskForm):
|
||||||
|
name = StringField('Facility Name', validators=[DataRequired(), Length(max=255)])
|
||||||
|
address = TextAreaField('Address', validators=[Optional()])
|
||||||
|
contact_person = StringField('Contact Person', validators=[Optional(), Length(max=100)])
|
||||||
|
contact_phone = StringField('Contact Phone', validators=[Optional(), Length(max=20)])
|
||||||
|
active = BooleanField('Active', default=True)
|
||||||
|
|
||||||
|
class AreaForm(FlaskForm):
|
||||||
|
name = StringField('Area Name', validators=[DataRequired(), Length(max=255)])
|
||||||
|
area_type = SelectField('Area Type', choices=[
|
||||||
|
('restroom', 'Restroom'),
|
||||||
|
('lobby', 'Lobby'),
|
||||||
|
('hallway', 'Hallway'),
|
||||||
|
('office', 'Office'),
|
||||||
|
('kitchen', 'Kitchen'),
|
||||||
|
('storage', 'Storage'),
|
||||||
|
('outdoor', 'Outdoor'),
|
||||||
|
('other', 'Other')
|
||||||
|
], validators=[Optional()])
|
||||||
|
facility_id = SelectField('Facility', coerce=int, validators=[DataRequired()])
|
||||||
|
|
||||||
|
class InspectionTemplateForm(FlaskForm):
|
||||||
|
name = StringField('Template Name', validators=[DataRequired(), Length(max=255)])
|
||||||
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
frequency = SelectField('Inspection Frequency', choices=[
|
||||||
|
('daily', 'Daily'),
|
||||||
|
('weekly', 'Weekly'),
|
||||||
|
('monthly', 'Monthly'),
|
||||||
|
('quarterly', 'Quarterly')
|
||||||
|
], validators=[DataRequired()])
|
||||||
|
|
||||||
|
class ChecklistItemForm(FlaskForm):
|
||||||
|
category = StringField('Category', validators=[DataRequired(), Length(max=100)])
|
||||||
|
item_description = TextAreaField('Item Description', validators=[DataRequired()])
|
||||||
|
scoring_type = SelectField('Scoring Type', choices=[
|
||||||
|
('pass_fail', 'Pass/Fail'),
|
||||||
|
('rating_5', '5-Point Rating'),
|
||||||
|
('rating_10', '10-Point Rating')
|
||||||
|
], validators=[DataRequired()])
|
||||||
|
weight = DecimalField('Weight', validators=[Optional(), NumberRange(min=0.1, max=10.0)], default=1.00)
|
||||||
|
requires_photo = BooleanField('Requires Photo Evidence', default=False)
|
||||||
|
display_order = IntegerField('Display Order', validators=[Optional()], default=0)
|
||||||
+13
-13
@@ -1,13 +1,13 @@
|
|||||||
Flask==3.0.0
|
Flask
|
||||||
Flask-SQLAlchemy==3.1.1
|
Flask-SQLAlchemy
|
||||||
Flask-Login==0.6.3
|
Flask-Login
|
||||||
Flask-WTF==1.2.1
|
Flask-WTF
|
||||||
Flask-Mail==0.9.1
|
Flask-Mail
|
||||||
Flask-Migrate==4.0.5
|
Flask-Migrate
|
||||||
PyMySQL==1.1.0
|
PyMySQL
|
||||||
cryptography==41.0.7
|
cryptography
|
||||||
python-dotenv==1.0.0
|
python-dotenv
|
||||||
Pillow==10.1.0
|
Pillow
|
||||||
WTForms==3.1.1
|
WTForms
|
||||||
email-validator==2.1.0
|
email-validator
|
||||||
gunicorn==21.2.0
|
gunicorn
|
||||||
|
|||||||
Reference in New Issue
Block a user