Feb 27 2026: update Issue view details

This commit is contained in:
2026-02-27 17:45:04 -05:00
parent 1ee621bf58
commit 0cf36a2f41
5 changed files with 72 additions and 6 deletions
+20
View File
@@ -2,6 +2,23 @@ from app import db
from datetime import datetime from datetime import datetime
class IssueComment(db.Model):
__tablename__ = 'issue_comments'
id = db.Column(db.Integer, primary_key=True)
issue_id = db.Column(db.Integer, db.ForeignKey('issues.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
status_at_time = db.Column(db.String(20)) # snapshot of issue status when comment was made
body = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
# Relationships
author = db.relationship('User', foreign_keys=[user_id])
def __repr__(self):
return f'<IssueComment {self.id} issue={self.issue_id}>'
class Issue(db.Model): class Issue(db.Model):
__tablename__ = 'issues' __tablename__ = 'issues'
@@ -20,6 +37,9 @@ class Issue(db.Model):
# Relationships # Relationships
assigned_user = db.relationship('User', foreign_keys=[assigned_to], backref='assigned_issues') assigned_user = db.relationship('User', foreign_keys=[assigned_to], backref='assigned_issues')
comments = db.relationship('IssueComment', backref='issue', lazy='dynamic',
order_by='IssueComment.created_at',
cascade='all, delete-orphan')
def __repr__(self): def __repr__(self):
return f'<Issue {self.id} - {self.severity}>' return f'<Issue {self.id} - {self.severity}>'
+16 -4
View File
@@ -3,7 +3,7 @@ from flask import (Blueprint, render_template, redirect, url_for,
flash, request, current_app) flash, request, current_app)
from flask_login import login_required, current_user from flask_login import login_required, current_user
from app import db from app import db
from app.models.issue import Issue from app.models.issue import Issue, IssueComment
from app.models.facility import Facility, Area from app.models.facility import Facility, Area
from app.models.user import User from app.models.user import User
from app.utils.forms import IssueForm, IssueUpdateForm from app.utils.forms import IssueForm, IssueUpdateForm
@@ -84,15 +84,27 @@ def view(issue_id):
existing = issue.result_photos or [] existing = issue.result_photos or []
issue.result_photos = existing + new_photos issue.result_photos = existing + new_photos
# Persist a comment entry if the user wrote update notes
comment_body = form.comments.data.strip() if form.comments.data else ''
if comment_body:
comment = IssueComment(
issue_id = issue.id,
user_id = current_user.id,
status_at_time = issue.status,
body = comment_body,
)
db.session.add(comment)
db.session.commit() db.session.commit()
current_app.logger.info( current_app.logger.info(
'ISSUE UPDATED | id=%s | status=%s | result_photos_added=%s | updated_by=%s', 'ISSUE UPDATED | id=%s | status=%s | result_photos_added=%s | comment=%s | updated_by=%s',
issue.id, issue.status, len(new_photos), current_user.username issue.id, issue.status, len(new_photos), bool(comment_body), current_user.username
) )
flash('Issue updated.', 'success') flash('Issue updated.', 'success')
return redirect(url_for('issues.view', issue_id=issue_id)) return redirect(url_for('issues.view', issue_id=issue_id))
return render_template('issues/view.html', issue=issue, form=form) comments = issue.comments.order_by(IssueComment.created_at.asc()).all()
return render_template('issues/view.html', issue=issue, form=form, comments=comments)
# ── Standalone create (not from an inspection) ──────────────────────────────── # ── Standalone create (not from an inspection) ────────────────────────────────
+7 -1
View File
@@ -46,7 +46,13 @@ def index():
total_inspections = base.count() total_inspections = base.count()
completed = base.filter(Inspection.status == 'completed').count() completed = base.filter(Inspection.status == 'completed').count()
flagged = base.filter(Inspection.status == 'flagged').count() # "Flagged" = open or in-progress issues logged within the date range,
# not inspections with status='flagged' (those get completed on submit).
flagged = Issue.query.filter(
Issue.reported_at >= start,
Issue.reported_at <= end,
Issue.status != 'resolved',
).count()
avg_score = db.session.query(func.avg(Inspection.overall_score)).filter( avg_score = db.session.query(func.avg(Inspection.overall_score)).filter(
Inspection.inspection_date >= start, Inspection.inspection_date >= start,
Inspection.inspection_date <= end, Inspection.inspection_date <= end,
+28
View File
@@ -69,6 +69,34 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{# ── Update History ─────────────────────────────────────────────────── #}
{% if comments %}
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h6 class="mb-0"><i class="bi bi-clock-history"></i> Update History</h6>
</div>
<ul class="list-group list-group-flush">
{% for c in comments %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-semibold text-dark">
<i class="bi bi-person-circle"></i> {{ c.author.username }}
</span>
<span class="d-flex align-items-center gap-2">
<span class="badge bg-{{ 'success' if c.status_at_time == 'resolved' else 'warning text-dark' if c.status_at_time == 'in_progress' else 'danger' }} rounded-pill" style="font-size:.65rem;">
{{ c.status_at_time|replace('_',' ')|title }}
</span>
<small class="text-muted">{{ c.created_at.strftime('%Y-%m-%d %H:%M') }}</small>
</span>
</div>
<p class="mb-0 text-secondary" style="white-space:pre-wrap; font-size:.9rem;">{{ c.body }}</p>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
+1 -1
View File
@@ -52,7 +52,7 @@
{% for label, value, color, icon in [ {% for label, value, color, icon in [
('Total Inspections', total_inspections, 'primary', 'bi-clipboard-data'), ('Total Inspections', total_inspections, 'primary', 'bi-clipboard-data'),
('Completed', completed, 'success', 'bi-check-circle'), ('Completed', completed, 'success', 'bi-check-circle'),
('Flagged', flagged, 'danger', 'bi-flag'), ('Open Issues', flagged, 'danger', 'bi-flag'),
('Avg Score', (avg_score|string + '%') if avg_score else '—', 'info', 'bi-graph-up'), ('Avg Score', (avg_score|string + '%') if avg_score else '—', 'info', 'bi-graph-up'),
] %} ] %}
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">