Feb 27 2026: fix Inspection result form
This commit is contained in:
+3
-1
@@ -15,9 +15,11 @@ class Issue(db.Model):
|
|||||||
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'))
|
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||||
reported_at = db.Column(db.DateTime, default=datetime.utcnow)
|
reported_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
resolved_at = db.Column(db.DateTime)
|
resolved_at = db.Column(db.DateTime)
|
||||||
|
result_notes = db.Column(db.Text)
|
||||||
|
result_photos = db.Column(db.JSON) # list of relative paths e.g. ["uploads/issue_photos/abc.jpg"]
|
||||||
|
|
||||||
# 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')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Issue {self.id} - {self.severity}>'
|
return f'<Issue {self.id} - {self.severity}>'
|
||||||
+33
-8
@@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import (Blueprint, render_template, redirect, url_for,
|
from flask import (Blueprint, render_template, redirect, url_for,
|
||||||
flash, request)
|
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
|
||||||
@@ -21,11 +21,9 @@ def index():
|
|||||||
|
|
||||||
q = Issue.query.order_by(Issue.reported_at.desc())
|
q = Issue.query.order_by(Issue.reported_at.desc())
|
||||||
|
|
||||||
# Inspectors only see issues they reported (linked to their inspections)
|
# Inspectors only see issues assigned to them
|
||||||
if current_user.role == 'inspector':
|
if current_user.role == 'inspector':
|
||||||
from app.models.inspection import Inspection
|
q = q.filter(Issue.assigned_to == current_user.id)
|
||||||
q = q.join(Inspection, Issue.inspection_id == Inspection.id)\
|
|
||||||
.filter(Inspection.inspector_id == current_user.id)
|
|
||||||
|
|
||||||
severity_filter = request.args.get('severity', '')
|
severity_filter = request.args.get('severity', '')
|
||||||
status_filter = request.args.get('status', '')
|
status_filter = request.args.get('status', '')
|
||||||
@@ -48,6 +46,12 @@ def index():
|
|||||||
@login_required
|
@login_required
|
||||||
def view(issue_id):
|
def view(issue_id):
|
||||||
issue = Issue.query.get_or_404(issue_id)
|
issue = Issue.query.get_or_404(issue_id)
|
||||||
|
|
||||||
|
# Access control: inspectors may only view/edit issues assigned to them
|
||||||
|
if current_user.role == 'inspector' and issue.assigned_to != current_user.id:
|
||||||
|
flash('Access denied. You can only view issues assigned to you.', 'danger')
|
||||||
|
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_(['supervisor','inspector'])).order_by(User.username).all()
|
||||||
@@ -55,15 +59,36 @@ def view(issue_id):
|
|||||||
form.status.data = form.status.data or issue.status
|
form.status.data = form.status.data or issue.status
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
issue.status = form.status.data
|
issue.status = form.status.data
|
||||||
issue.assigned_to = form.assigned_to.data or None
|
|
||||||
|
# Only admin/supervisor can reassign; inspectors can only update status
|
||||||
|
if current_user.role in ['admin', 'supervisor']:
|
||||||
|
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:
|
||||||
issue.resolved_at = datetime.utcnow()
|
issue.resolved_at = datetime.utcnow()
|
||||||
elif form.status.data != 'resolved':
|
elif form.status.data != 'resolved':
|
||||||
issue.resolved_at = None
|
issue.resolved_at = None
|
||||||
|
|
||||||
|
# Save result notes (overwrite with latest value)
|
||||||
|
issue.result_notes = form.result_notes.data or None
|
||||||
|
|
||||||
|
# Append any newly uploaded result photos
|
||||||
|
from app.routes.inspections import _save_photo
|
||||||
|
new_photos = []
|
||||||
|
for file_obj in request.files.getlist('result_photos'):
|
||||||
|
path = _save_photo(file_obj, subfolder='issue_result_photos')
|
||||||
|
if path:
|
||||||
|
new_photos.append(path)
|
||||||
|
if new_photos:
|
||||||
|
existing = issue.result_photos or []
|
||||||
|
issue.result_photos = existing + new_photos
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
current_app.logger.info(
|
||||||
|
'ISSUE UPDATED | id=%s | status=%s | result_photos_added=%s | updated_by=%s',
|
||||||
|
issue.id, issue.status, len(new_photos), 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))
|
||||||
|
|
||||||
@@ -100,4 +125,4 @@ def create():
|
|||||||
flash('Issue created.', 'success')
|
flash('Issue created.', 'success')
|
||||||
return redirect(url_for('issues.index'))
|
return redirect(url_for('issues.index'))
|
||||||
|
|
||||||
return render_template('issues/form.html', form=form, title='Log New Issue')
|
return render_template('issues/form.html', form=form, title='Log New Issue')
|
||||||
@@ -71,7 +71,15 @@
|
|||||||
{% if issue.assigned_user %}{{ issue.assigned_user.username }}
|
{% if issue.assigned_user %}{{ issue.assigned_user.username }}
|
||||||
{% else %}<span class="text-muted">—</span>{% endif %}
|
{% else %}<span class="text-muted">—</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><a href="{{ url_for('issues.view', issue_id=issue.id) }}" class="btn btn-sm btn-outline-secondary">View</a></td>
|
<td>
|
||||||
|
<a href="{{ url_for('issues.view', issue_id=issue.id) }}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
{% if current_user.role in ['admin','supervisor'] or issue.assigned_to == current_user.id %}
|
||||||
|
<i class="bi bi-pencil"></i> Edit
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-eye"></i> View
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -95,4 +103,4 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -48,29 +48,68 @@
|
|||||||
<img src="{{ url_for('static', filename=issue.photo_path) }}" class="img-fluid rounded" style="max-height:300px;">
|
<img src="{{ url_for('static', filename=issue.photo_path) }}" class="img-fluid rounded" style="max-height:300px;">
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if issue.result_notes or issue.result_photos %}
|
||||||
|
<hr>
|
||||||
|
<h6><i class="bi bi-clipboard2-check text-success"></i> Resolution Details</h6>
|
||||||
|
{% if issue.result_notes %}
|
||||||
|
<p class="mb-2" style="white-space:pre-wrap;">{{ issue.result_notes }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if issue.result_photos %}
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||||
|
{% for photo in issue.result_photos %}
|
||||||
|
<a href="{{ url_for('static', filename=photo) }}" target="_blank">
|
||||||
|
<img src="{{ url_for('static', filename=photo) }}"
|
||||||
|
class="rounded border" style="max-height:120px; max-width:160px; object-fit:cover;"
|
||||||
|
alt="Result photo">
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
{% if current_user.role in ['admin','supervisor'] %}
|
{% set can_edit = current_user.role in ['admin','supervisor'] or issue.assigned_to == current_user.id %}
|
||||||
|
{% 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>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ 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'] %}
|
||||||
<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") }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ form.comments.label(class="form-label fw-semibold") }}
|
{{ form.comments.label(class="form-label fw-semibold") }}
|
||||||
{{ form.comments(class="form-control", rows=3, placeholder="Optional update notes…") }}
|
{{ form.comments(class="form-control", rows=3, placeholder="Optional update notes…") }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.result_notes.label(class="form-label fw-semibold") }}
|
||||||
|
{{ form.result_notes(class="form-control", rows=3,
|
||||||
|
placeholder="Describe what was done to resolve this issue…",
|
||||||
|
value=issue.result_notes or '') }}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Result Photos</label>
|
||||||
|
<input type="file" name="result_photos" id="result_photos"
|
||||||
|
class="form-control" accept="image/*" multiple>
|
||||||
|
<div class="form-text">Attach one or more photos showing the resolution.</div>
|
||||||
|
{% if issue.result_photos %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">{{ issue.result_photos|length }} photo(s) already uploaded</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn btn-primary w-100">Save Update</button>
|
<button type="submit" class="btn btn-primary w-100">Save Update</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,4 +121,4 @@
|
|||||||
<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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+8
-3
@@ -123,8 +123,13 @@ class IssueForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class IssueUpdateForm(FlaskForm):
|
class IssueUpdateForm(FlaskForm):
|
||||||
status = SelectField('Status', choices=[
|
status = SelectField('Status', choices=[
|
||||||
('open','Open'), ('in_progress','In Progress'), ('resolved','Resolved'),
|
('open','Open'), ('in_progress','In Progress'), ('resolved','Resolved'),
|
||||||
], validators=[DataRequired()])
|
], validators=[DataRequired()])
|
||||||
assigned_to = SelectField('Assign To', coerce=int, validators=[Optional()])
|
assigned_to = SelectField('Assign To', coerce=int, validators=[Optional()])
|
||||||
comments = TextAreaField('Update Notes', validators=[Optional(), Length(max=1000)])
|
comments = TextAreaField('Update Notes', validators=[Optional(), Length(max=1000)])
|
||||||
|
result_notes = TextAreaField('Result Notes', validators=[Optional(), Length(max=2000)])
|
||||||
|
result_photos = FileField('Result Photos', validators=[
|
||||||
|
Optional(),
|
||||||
|
FileAllowed(['jpg','jpeg','png','gif'], 'Images only.')
|
||||||
|
])
|
||||||
Reference in New Issue
Block a user