diff --git a/app/models/issue.py b/app/models/issue.py index b68f1fc..cfbf644 100644 --- a/app/models/issue.py +++ b/app/models/issue.py @@ -15,9 +15,11 @@ class Issue(db.Model): assigned_to = db.Column(db.Integer, db.ForeignKey('users.id')) reported_at = db.Column(db.DateTime, default=datetime.utcnow) 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 assigned_user = db.relationship('User', foreign_keys=[assigned_to], backref='assigned_issues') def __repr__(self): - return f'' + return f'' \ No newline at end of file diff --git a/app/routes/issues.py b/app/routes/issues.py index 3ecb93a..3447183 100644 --- a/app/routes/issues.py +++ b/app/routes/issues.py @@ -1,6 +1,6 @@ from datetime import datetime from flask import (Blueprint, render_template, redirect, url_for, - flash, request) + flash, request, current_app) from flask_login import login_required, current_user from app import db from app.models.issue import Issue @@ -21,11 +21,9 @@ def index(): 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': - from app.models.inspection import Inspection - q = q.join(Inspection, Issue.inspection_id == Inspection.id)\ - .filter(Inspection.inspector_id == current_user.id) + q = q.filter(Issue.assigned_to == current_user.id) severity_filter = request.args.get('severity', '') status_filter = request.args.get('status', '') @@ -48,6 +46,12 @@ def index(): @login_required def view(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) 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 if form.validate_on_submit(): - issue.status = form.status.data - issue.assigned_to = form.assigned_to.data or None + issue.status = form.status.data + + # 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: issue.resolved_at = datetime.utcnow() elif form.status.data != 'resolved': 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() + 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') return redirect(url_for('issues.view', issue_id=issue_id)) @@ -100,4 +125,4 @@ def create(): flash('Issue created.', 'success') 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') \ No newline at end of file diff --git a/app/templates/issues/list.html b/app/templates/issues/list.html index 47673b4..207e744 100644 --- a/app/templates/issues/list.html +++ b/app/templates/issues/list.html @@ -71,7 +71,15 @@ {% if issue.assigned_user %}{{ issue.assigned_user.username }} {% else %}{% endif %} - View + + + {% if current_user.role in ['admin','supervisor'] or issue.assigned_to == current_user.id %} + Edit + {% else %} + View + {% endif %} + + {% endfor %} @@ -95,4 +103,4 @@ {% endif %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/issues/view.html b/app/templates/issues/view.html index f9043f8..d829284 100644 --- a/app/templates/issues/view.html +++ b/app/templates/issues/view.html @@ -48,29 +48,68 @@ {% endif %} + + {% if issue.result_notes or issue.result_photos %} +
+
Resolution Details
+ {% if issue.result_notes %} +

{{ issue.result_notes }}

+ {% endif %} + {% if issue.result_photos %} +
+ {% for photo in issue.result_photos %} + + Result photo + + {% endfor %} +
+ {% endif %} + {% endif %}
- {% 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 %}
Update Issue
-
+
{{ form.status.label(class="form-label fw-semibold") }} {{ form.status(class="form-select") }}
+ {% if current_user.role in ['admin','supervisor'] %}
{{ form.assigned_to.label(class="form-label fw-semibold") }} {{ form.assigned_to(class="form-select") }}
+ {% endif %}
{{ form.comments.label(class="form-label fw-semibold") }} {{ form.comments(class="form-control", rows=3, placeholder="Optional update notes…") }}
+
+ {{ 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 '') }} +
+
+ + +
Attach one or more photos showing the resolution.
+ {% if issue.result_photos %} +
+ {{ issue.result_photos|length }} photo(s) already uploaded +
+ {% endif %} +
@@ -82,4 +121,4 @@ Back to Issues -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/app/utils/forms.py b/app/utils/forms.py index cb79737..fdf57c1 100644 --- a/app/utils/forms.py +++ b/app/utils/forms.py @@ -123,8 +123,13 @@ class IssueForm(FlaskForm): class IssueUpdateForm(FlaskForm): - status = SelectField('Status', choices=[ + status = SelectField('Status', choices=[ ('open','Open'), ('in_progress','In Progress'), ('resolved','Resolved'), ], validators=[DataRequired()]) - assigned_to = SelectField('Assign To', coerce=int, validators=[Optional()]) - comments = TextAreaField('Update Notes', validators=[Optional(), Length(max=1000)]) + assigned_to = SelectField('Assign To', coerce=int, validators=[Optional()]) + 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.') + ]) \ No newline at end of file