Mar 04 2026: Implement some operational features: scheduled reports, issue resolution verification, etc - Phase 6

This commit is contained in:
2026-03-04 17:14:06 -05:00
parent 160f9fec5d
commit 7c0ed93795
22 changed files with 1787 additions and 19 deletions
+100
View File
@@ -117,6 +117,14 @@ def view(issue_id):
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
form.status.data = form.status.data or issue.status
# Extend status choices to include pending_verification
form.status.choices = [
('open', 'Open'),
('in_progress', 'In Progress'),
('pending_verification', 'Pending Verification'),
('resolved', 'Resolved'),
]
if form.validate_on_submit():
old_status = issue.status
old_assigned_to = issue.assigned_to
@@ -416,4 +424,96 @@ def create():
flash('Issue created.', 'success')
return redirect(url_for('issues.index'))
# ── Supervisor verify resolved issue ─────────────────────────────────────────
@bp.route('/<int:issue_id>/verify', methods=['POST'])
@login_required
@supervisor_required
def verify(issue_id):
"""Supervisor sign-off: confirms resolution is satisfactory and closes the issue."""
issue = Issue.query.get_or_404(issue_id)
if issue.status not in ('resolved', 'pending_verification'):
flash('Only resolved or pending-verification issues can be verified.', 'warning')
return redirect(url_for('issues.view', issue_id=issue_id))
note = request.form.get('verification_note', '').strip() or None
issue.status = 'resolved'
issue.verified_by = current_user.id
issue.verified_at = now_eastern()
issue.verification_note = note
if not issue.resolved_at:
issue.resolved_at = now_eastern()
db.session.commit()
current_app.logger.info(
'ISSUE VERIFIED | id=%s | by=%s | note=%r',
issue_id, current_user.username, note,
)
log_action(ACTION_UPDATE, 'Issue', issue_id,
f'#{issue_id} in {issue.area.name}',
f'verified_by={current_user.username}')
flash(f'Issue #{issue_id} verified and closed.', 'success')
return redirect(url_for('issues.view', issue_id=issue_id))
@bp.route('/<int:issue_id>/request-verification', methods=['POST'])
@login_required
def request_verification(issue_id):
"""Inspector/assignee marks the issue as pending supervisor verification."""
issue = Issue.query.get_or_404(issue_id)
if current_user.role == 'customer':
flash('Access denied.', 'danger')
return redirect(url_for('issues.index'))
# Only the assignee, supervisor, or admin can request verification
can_act = (
current_user.role in ['admin', 'supervisor']
or issue.assigned_to == current_user.id
)
if not can_act:
flash('Access denied.', 'danger')
return redirect(url_for('issues.view', issue_id=issue_id))
if issue.status not in ('in_progress', 'resolved'):
flash('Issue must be in progress or resolved to request verification.', 'warning')
return redirect(url_for('issues.view', issue_id=issue_id))
issue.status = 'pending_verification'
db.session.commit()
current_app.logger.info(
'ISSUE VERIFICATION REQUESTED | id=%s | by=%s',
issue_id, current_user.username,
)
log_action(ACTION_UPDATE, 'Issue', issue_id,
f'#{issue_id} in {issue.area.name}',
f'status=pending_verification; requested_by={current_user.username}')
# Notify supervisors
from app.utils.notifications import notify
from app.models.notification import EVENT_ISSUE_STATUS
supervisors = User.query.filter(User.role.in_(['admin', 'supervisor'])).all()
for sup in supervisors:
if sup.id != current_user.id:
notify(
recipient = sup,
title = f'Issue #{issue_id} Awaiting Verification',
body = (
f'{current_user.username} has marked Issue #{issue_id} '
f'({issue.severity.title()} severity) in {issue.area.name} '
f'as pending your verification.'
),
link = url_for('issues.view', issue_id=issue_id),
issue_id = issue_id,
event_type = EVENT_ISSUE_STATUS,
send_email = True,
)
db.session.commit()
flash('Issue marked as pending verification. Supervisors have been notified.', 'info')
return redirect(url_for('issues.view', issue_id=issue_id))
return render_template('issues/form.html', form=form, title='Log New Issue')