Aug 3 - Update scheduled task acknowledged
This commit is contained in:
@@ -65,6 +65,11 @@ def _scheduled_payload(s):
|
||||
# run lands as a linked re-inspection. Additive — older builds decode
|
||||
# explicit CodingKeys and ignore it.
|
||||
'parent_inspection_id': s.parent_inspection_id,
|
||||
# phase47 — receipt acknowledgement. Read-only here: this collection is
|
||||
# read-only per CLAUDE.md rule 77, so confirming happens on the web. The
|
||||
# iPad can display "confirmed" state from this timestamp (NULL = the
|
||||
# assigned inspector has not confirmed receipt yet).
|
||||
'acknowledged_at': s.acknowledged_at.isoformat() if s.acknowledged_at else None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -130,6 +130,16 @@ class ScheduledInspection(db.Model):
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=now_eastern)
|
||||
last_completed_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# ── Receipt acknowledgement (phase47) ────────────────────────────────────
|
||||
# When the assigned inspector confirms they have received/seen this
|
||||
# assignment. Once per assignment: NULL means "awaiting confirmation"; it is
|
||||
# reset to NULL when the schedule is reassigned to a different inspector so
|
||||
# the new assignee must confirm afresh. It is NOT reset when a recurring
|
||||
# schedule rolls forward — the acknowledgement is of the assignment, not of
|
||||
# each occurrence. The acknowledger is always `inspector` (the only person
|
||||
# allowed to confirm), so no separate acknowledged_by column is needed.
|
||||
acknowledged_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Per-occurrence reminder de-dup flags (reset when a recurring one rolls forward)
|
||||
advance_notified = db.Column(db.Boolean, nullable=False, default=False)
|
||||
due_notified = db.Column(db.Boolean, nullable=False, default=False)
|
||||
@@ -269,6 +279,11 @@ class ScheduledInspection(db.Model):
|
||||
"""
|
||||
return self.end_date is None or d <= self.end_date
|
||||
|
||||
@property
|
||||
def is_acknowledged(self):
|
||||
"""True once the assigned inspector has confirmed receipt (phase47)."""
|
||||
return self.acknowledged_at is not None
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
"""True once the end date has passed.
|
||||
|
||||
@@ -80,6 +80,28 @@ def _notify_assignee(sched, reassigned=False):
|
||||
)
|
||||
|
||||
|
||||
def _notify_creator_acknowledged(sched):
|
||||
"""Notify the schedule's creator that the assigned inspector has confirmed
|
||||
receipt of the request. No-op when there is no creator, the creator is
|
||||
inactive, or the creator IS the inspector (self-assigned). Caller commits."""
|
||||
creator = sched.creator
|
||||
if not creator or not creator.active or creator.id == sched.inspector_id:
|
||||
return
|
||||
fac = sched.facility.name if sched.facility else '—'
|
||||
tpl = sched.template.name if sched.template else '—'
|
||||
who = sched.inspector.display_name if sched.inspector else 'The inspector'
|
||||
notify(
|
||||
recipient = creator,
|
||||
title = f'Inspector confirmed receipt — {fac}',
|
||||
body = (f'{who} confirmed receipt of the "{tpl}" scheduled '
|
||||
f'inspection at {fac} ({sched.frequency_label.lower()}), '
|
||||
f'due {sched.next_due_date:%b %d, %Y}.'),
|
||||
link = url_for('scheduled_inspections.index'),
|
||||
event_type = EVENT_SCHEDULED_INSPECTION,
|
||||
send_email = True,
|
||||
)
|
||||
|
||||
|
||||
def _apply_recurrence(sched, form):
|
||||
"""Copy the recurrence block for the chosen frequency onto *sched* and
|
||||
clear the blocks that no longer apply, then snap next_due_date onto the
|
||||
@@ -260,6 +282,10 @@ def edit(schedule_id):
|
||||
sched.inspector_id = form.inspector_id.data
|
||||
sched.notes = (form.notes.data or '').strip() or None
|
||||
sched.active = form.active.data
|
||||
# Reassigning to a different inspector invalidates any prior confirmation
|
||||
# — the new assignee has not yet acknowledged the request (phase47).
|
||||
if sched.inspector_id != old_inspector_id:
|
||||
sched.acknowledged_at = None
|
||||
_apply_recurrence(sched, form)
|
||||
if _reject_if_past_end_date(sched, form):
|
||||
# sched is a persistent object and has already been mutated — discard
|
||||
@@ -374,6 +400,39 @@ def start(schedule_id):
|
||||
return redirect(url_for('inspections.execute', inspection_id=inspection.id))
|
||||
|
||||
|
||||
# ── Acknowledge (inspector confirms receipt of the request) ───────────────────
|
||||
|
||||
@bp.route('/<int:schedule_id>/acknowledge', methods=['POST'])
|
||||
@login_required
|
||||
def acknowledge(schedule_id):
|
||||
"""The assigned inspector confirms they have received the scheduled request.
|
||||
|
||||
Assignee-only (like Start): a manager cannot confirm on someone's behalf.
|
||||
Idempotent — confirming an already-confirmed schedule is a no-op. On the
|
||||
first confirmation the schedule's creator is notified."""
|
||||
sched = db.session.get(ScheduledInspection, schedule_id)
|
||||
if sched is None:
|
||||
abort(404)
|
||||
if not sched.inspector_id or sched.inspector_id != current_user.id:
|
||||
abort(403)
|
||||
|
||||
if sched.acknowledged_at is None:
|
||||
sched.acknowledged_at = now_eastern()
|
||||
db.session.commit()
|
||||
log_action(ACTION_UPDATE, 'ScheduledInspection', sched.id,
|
||||
f'{sched.template.name if sched.template else "?"} @ '
|
||||
f'{sched.facility.name if sched.facility else "?"}',
|
||||
'inspector confirmed receipt')
|
||||
logger.info('SCHED INSP | acknowledge | schedule=%s | by=%s',
|
||||
sched.id, current_user.username)
|
||||
_notify_creator_acknowledged(sched)
|
||||
db.session.commit()
|
||||
flash('You have confirmed receipt of this scheduled inspection.', 'success')
|
||||
else:
|
||||
flash('You have already confirmed this scheduled inspection.', 'info')
|
||||
return redirect(url_for('scheduled_inspections.index'))
|
||||
|
||||
|
||||
# ── Cron: reminders (advance / due / overdue) ─────────────────────────────────
|
||||
|
||||
@bp.route('/run', methods=['POST'])
|
||||
|
||||
@@ -61,6 +61,16 @@
|
||||
<td class="text-end">
|
||||
{# Start is shown only to the assignee — the inspection is theirs to do. #}
|
||||
{% if s.inspector_id and s.inspector_id == current_user.id %}
|
||||
{# Receipt confirmation (phase47) — assignee confirms, or shows confirmed. #}
|
||||
{% if s.is_acknowledged %}
|
||||
<span class="badge bg-success" title="You confirmed receipt"><i class="bi bi-check-circle"></i> Confirmed</span>
|
||||
{% else %}
|
||||
<form method="POST" class="d-inline" action="{{ url_for('scheduled_inspections.acknowledge', schedule_id=s.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-success py-0" title="Confirm you received this request">
|
||||
<i class="bi bi-check-lg"></i> Confirm</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% set open_id = sched_open_inspections.get(s.id) %}
|
||||
{% if open_id %}
|
||||
<a href="{{ url_for('inspections.execute', inspection_id=open_id) }}"
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<th>Next Due</th>
|
||||
<th>Ends</th>
|
||||
<th>Status</th>
|
||||
<th>Confirmation</th>
|
||||
<th>Created By</th>
|
||||
<th class="text-end"></th>
|
||||
</tr>
|
||||
@@ -74,6 +75,31 @@
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{# Receipt confirmation by the assigned inspector (phase47). #}
|
||||
{% if not s.inspector_id %}
|
||||
<span class="text-muted">—</span>
|
||||
{% elif s.is_acknowledged %}
|
||||
<span class="badge bg-success" title="Confirmed by {{ s.inspector.display_name if s.inspector else 'inspector' }}">
|
||||
<i class="bi bi-check-circle"></i> Confirmed
|
||||
</span>
|
||||
<div class="small text-muted">{{ s.acknowledged_at.strftime('%b %d, %Y') }}</div>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark" title="The inspector has not confirmed receipt yet">
|
||||
<i class="bi bi-hourglass-split"></i> Awaiting
|
||||
</span>
|
||||
{# The assignee can confirm right here. #}
|
||||
{% if s.inspector_id == current_user.id %}
|
||||
<form method="POST" class="d-inline"
|
||||
action="{{ url_for('scheduled_inspections.acknowledge', schedule_id=s.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-success mt-1" title="Confirm you received this request">
|
||||
<i class="bi bi-check-lg"></i> Confirm receipt
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ s.creator.display_name if s.creator else '—' }}
|
||||
<div class="small text-muted">{{ s.created_at.strftime('%b %d, %Y') if s.created_at else '' }}</div>
|
||||
|
||||
Reference in New Issue
Block a user