Aug 3 - Update scheduled task acknowledged
This commit is contained in:
@@ -9,7 +9,8 @@
|
|||||||
"Bash(grep -n -A3 dateOnlyFormatter __TRACKED_VAR__/Sync/SyncManager.swift)",
|
"Bash(grep -n -A3 dateOnlyFormatter __TRACKED_VAR__/Sync/SyncManager.swift)",
|
||||||
"Bash(python -c \"import ast; ast.parse\\(open\\('app/utils/pdf_export.py'\\).read\\(\\)\\); print\\('OK'\\)\")",
|
"Bash(python -c \"import ast; ast.parse\\(open\\('app/utils/pdf_export.py'\\).read\\(\\)\\); print\\('OK'\\)\")",
|
||||||
"Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/utils/pdf_export.py',encoding='utf-8'\\).read\\(\\)\\); print\\('OK'\\)\")",
|
"Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/utils/pdf_export.py',encoding='utf-8'\\).read\\(\\)\\); print\\('OK'\\)\")",
|
||||||
"Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/routes/issues.py',encoding='utf-8'\\).read\\(\\)\\); print\\('OK'\\)\")"
|
"Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/routes/issues.py',encoding='utf-8'\\).read\\(\\)\\); print\\('OK'\\)\")",
|
||||||
|
"Bash(python -c \"print\\(len\\('phase47_sched_acknowledged'\\)\\)\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -422,7 +422,8 @@ scheduled_inspections:
|
|||||||
month_mode VARCHAR(20) nullable, -- Phase 43: 'day_of_month' | 'nth_weekday'
|
month_mode VARCHAR(20) nullable, -- Phase 43: 'day_of_month' | 'nth_weekday'
|
||||||
day_of_month SMALLINT nullable, -- Phase 43
|
day_of_month SMALLINT nullable, -- Phase 43
|
||||||
nth_week SMALLINT nullable, -- Phase 43: 1–5, or -1 = last
|
nth_week SMALLINT nullable, -- Phase 43: 1–5, or -1 = last
|
||||||
nth_weekday SMALLINT nullable -- Phase 43: 0–6, Mon=0
|
nth_weekday SMALLINT nullable, -- Phase 43: 0–6, Mon=0
|
||||||
|
acknowledged_at DATETIME nullable -- Phase 47: inspector confirmed receipt
|
||||||
inspections.scheduled_inspection_id FK→scheduled_inspections SET NULL ← Phase 36
|
inspections.scheduled_inspection_id FK→scheduled_inspections SET NULL ← Phase 36
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -452,6 +453,7 @@ All are nullable and **legacy phase36 rows keep NULLs**, falling back to `_add_i
|
|||||||
- **Form validation.** `ScheduledInspectionForm.validate()` rejects an end date on a one-time schedule and one earlier than the due date. That is not sufficient alone: `align_due_date()` can push the picked date forward onto the rule (a Tuesday pick on a Mon/Wed/Fri schedule becomes Wednesday), so `_reject_if_past_end_date()` re-checks after `_apply_recurrence()` in both create and edit. Edit rolls back first — `sched` is persistent and already mutated at that point.
|
- **Form validation.** `ScheduledInspectionForm.validate()` rejects an end date on a one-time schedule and one earlier than the due date. That is not sufficient alone: `align_due_date()` can push the picked date forward onto the rule (a Tuesday pick on a Mon/Wed/Fri schedule becomes Wednesday), so `_reject_if_past_end_date()` re-checks after `_apply_recurrence()` in both create and edit. Edit rolls back first — `sched` is persistent and already mutated at that point.
|
||||||
- **Three status states** in the list: Active, **Ended** (`is_expired` — ran its course), Inactive (a manager switched it off).
|
- **Three status states** in the list: Active, **Ended** (`is_expired` — ran its course), Inactive (a manager switched it off).
|
||||||
- **Assignment notification** (immediate): on **create**, the assigned inspector gets an in-app + email "assigned to you" notification; on **edit**, only when the inspector actually changes (a "reassigned to you" notification to the new assignee). Via `_notify_assignee()` in the blueprint using `event_type=EVENT_SCHEDULED_INSPECTION`.
|
- **Assignment notification** (immediate): on **create**, the assigned inspector gets an in-app + email "assigned to you" notification; on **edit**, only when the inspector actually changes (a "reassigned to you" notification to the new assignee). Via `_notify_assignee()` in the blueprint using `event_type=EVENT_SCHEDULED_INSPECTION`.
|
||||||
|
- **Receipt acknowledgement (phase47).** `acknowledged_at` records when the assigned inspector confirms they received the request — a way for the manager to see the assignment was seen, distinct from starting it. `POST /<id>/acknowledge` is **assignee-only** (like Start; a manager cannot confirm on someone's behalf) and idempotent; on first confirm the schedule's **creator** is notified via `_notify_creator_acknowledged()` (`event_type=EVENT_SCHEDULED_INSPECTION`, skipped when creator is inactive or is the inspector). **Once per assignment, not per occurrence:** it is NOT reset when a recurring schedule rolls forward (`fulfill()` leaves it), but the **edit route resets it to NULL when the inspector changes** so a new assignee must re-confirm. Surfaced with a Confirmed/Awaiting badge + a "Confirm receipt" button (assignee only) on both the scheduled-inspections **list** and the **dashboard** panel. `ScheduledInspection.is_acknowledged` is the boolean helper. Exposed **read-only** in the API payload (`acknowledged_at`) — confirming stays on the web per rule 77; an iPad confirm action would be a follow-up.
|
||||||
- **Reminders** are dispatched by the cron endpoint (see §11): advance (1 day before) + due-date to the inspector, overdue to admin/director — each fires at most once per occurrence via the `*_notified` flags. Uses `notify()` with `event_type=EVENT_SCHEDULED_INSPECTION`.
|
- **Reminders** are dispatched by the cron endpoint (see §11): advance (1 day before) + due-date to the inspector, overdue to admin/director — each fires at most once per occurrence via the `*_notified` flags. Uses `notify()` with `event_type=EVENT_SCHEDULED_INSPECTION`.
|
||||||
- Dashboard shows an **upcoming (next 7 days) / overdue** panel for non-customers (inspectors see only their own).
|
- Dashboard shows an **upcoming (next 7 days) / overdue** panel for non-customers (inspectors see only their own).
|
||||||
- **Instructions (July 2026).** `ScheduledInspectionForm.notes` is labelled **"Instructions"** and `scheduled_inspections/form.html` explains that the text reaches the inspector. The *field name*, `ScheduledInspection.notes`, the `scheduled_inspections.notes` column and the API key `notes` are all unchanged — the rename is a label only (rule 84). The text is surfaced to the inspector in two places: `inspections/execute.html` renders an indigo panel between the header and the form grid, guarded on `inspection.scheduled_inspection and .notes` (NULL for ad-hoc work and for schedules deleted after the start); the iPad shows it on the scheduled row, on the start screen and above the form.
|
- **Instructions (July 2026).** `ScheduledInspectionForm.notes` is labelled **"Instructions"** and `scheduled_inspections/form.html` explains that the text reaches the inspector. The *field name*, `ScheduledInspection.notes`, the `scheduled_inspections.notes` column and the API key `notes` are all unchanged — the rename is a label only (rule 84). The text is surfaced to the inspector in two places: `inspections/execute.html` renders an indigo panel between the header and the form grid, guarded on `inspection.scheduled_inspection and .notes` (NULL for ad-hoc work and for schedules deleted after the start); the iPad shows it on the scheduled row, on the start screen and above the form.
|
||||||
@@ -519,7 +521,7 @@ Management (`/scheduled-inspections/new|edit|delete`) is `@project_manager_requi
|
|||||||
| `audit` | `/audit` | list (admin only), view, purge |
|
| `audit` | `/audit` | list (admin only), view, purge |
|
||||||
| `reports` | `/reports` | index, facility report, scorecard, CSV/PDF/Excel export, issues-aging, sla-compliance, followup-closure, facility summary PDF |
|
| `reports` | `/reports` | index, facility report, scorecard, CSV/PDF/Excel export, issues-aging, sla-compliance, followup-closure, facility summary PDF |
|
||||||
| `scheduled_reports` | `/scheduled-reports` | CRUD + manual trigger (accessible via Reports sub-nav) |
|
| `scheduled_reports` | `/scheduled-reports` | CRUD + manual trigger (accessible via Reports sub-nav) |
|
||||||
| `scheduled_inspections` | `/scheduled-inspections` | list, new/edit/delete (PM+), `GET /<id>/start` (**assigned inspector only** → creates linked inspection; 403 for non-assignees incl. managers), `POST /run` (cron reminders, `token=DIGEST_SECRET`) |
|
| `scheduled_inspections` | `/scheduled-inspections` | list, new/edit/delete (PM+), `GET /<id>/start` (**assigned inspector only** → creates linked inspection; 403 for non-assignees incl. managers), `POST /<id>/acknowledge` (**assigned inspector only** → confirms receipt, sets `acknowledged_at`, notifies creator; idempotent — Phase 47), `POST /run` (cron reminders, `token=DIGEST_SECRET`) |
|
||||||
| `support` | `/support` | `GET /chat` (loads latest saved session; `?new=1` to start fresh), `POST /chat/message` (AJAX→Groq; **persists** user+assistant turns, returns `session_id`), `GET /my-conversations`, `GET /my-conversations/<id>` (customer chat history), `GET /admin/conversations`, `GET /admin/conversations/<id>` (staff, read-only), `GET /admin/knowledge` + `/new`, `/<id>/edit`, `/<id>/delete` (admin/director — chatbot knowledge base), `POST /tickets`, `GET /my-tickets`, `GET/POST /my-tickets/<id>`, `GET /admin/tickets`, `GET/POST /admin/tickets/<id>` |
|
| `support` | `/support` | `GET /chat` (loads latest saved session; `?new=1` to start fresh), `POST /chat/message` (AJAX→Groq; **persists** user+assistant turns, returns `session_id`), `GET /my-conversations`, `GET /my-conversations/<id>` (customer chat history), `GET /admin/conversations`, `GET /admin/conversations/<id>` (staff, read-only), `GET /admin/knowledge` + `/new`, `/<id>/edit`, `/<id>/delete` (admin/director — chatbot knowledge base), `POST /tickets`, `GET /my-tickets`, `GET/POST /my-tickets/<id>`, `GET /admin/tickets`, `GET/POST /admin/tickets/<id>` |
|
||||||
| `broadcast` | `/admin/broadcast` | `GET /` (compose + history), `POST /send` (admin-only; fans out one Notification per targeted user) |
|
| `broadcast` | `/admin/broadcast` | `GET /` (compose + history), `POST /send` (admin-only; fans out one Notification per targeted user) |
|
||||||
| `devices` | `/admin/devices` | `GET /` (device list from `api_device_tokens`), `POST /notify` (admin-only) |
|
| `devices` | `/admin/devices` | `GET /` (device list from `api_device_tokens`), `POST /notify` (admin-only) |
|
||||||
@@ -883,7 +885,18 @@ phase1_projects_roles → phase6_features → phase7_mobile_api → phase8_notif
|
|||||||
→ phase43_sched_recurrence
|
→ phase43_sched_recurrence
|
||||||
→ phase44_sched_end_date
|
→ phase44_sched_end_date
|
||||||
→ phase45_sched_parent_insp
|
→ phase45_sched_parent_insp
|
||||||
→ phase46_followup_req_by ← HEAD
|
→ phase46_followup_req_by
|
||||||
|
→ phase47_sched_acknowledged ← HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
#### phase47 — scheduled inspection receipt acknowledgement
|
||||||
|
|
||||||
|
Revision id `phase47_sched_acknowledged` (file `phase47_sched_acknowledged.py`, down_revision `phase46_followup_req_by`). Adds `scheduled_inspections.acknowledged_at DATETIME NULL` — when the assigned inspector confirms they received the scheduled request. Backs the **receipt acknowledgement** feature — see §5 `ScheduledInspection` "Receipt acknowledgement". **No backfill**: legacy rows keep NULL and render as "Awaiting" confirmation, the correct initial state. `INFORMATION_SCHEMA` column check — safe to re-run.
|
||||||
|
|
||||||
|
**Deploy order:**
|
||||||
|
```bash
|
||||||
|
flask db upgrade
|
||||||
|
sudo systemctl restart gunicorn
|
||||||
```
|
```
|
||||||
|
|
||||||
#### phase46 — follow-up request attribution
|
#### phase46 — follow-up request attribution
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ def _scheduled_payload(s):
|
|||||||
# run lands as a linked re-inspection. Additive — older builds decode
|
# run lands as a linked re-inspection. Additive — older builds decode
|
||||||
# explicit CodingKeys and ignore it.
|
# explicit CodingKeys and ignore it.
|
||||||
'parent_inspection_id': s.parent_inspection_id,
|
'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)
|
created_at = db.Column(db.DateTime, nullable=False, default=now_eastern)
|
||||||
last_completed_at = db.Column(db.DateTime, nullable=True)
|
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)
|
# Per-occurrence reminder de-dup flags (reset when a recurring one rolls forward)
|
||||||
advance_notified = db.Column(db.Boolean, nullable=False, default=False)
|
advance_notified = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
due_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
|
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
|
@property
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
"""True once the end date has passed.
|
"""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):
|
def _apply_recurrence(sched, form):
|
||||||
"""Copy the recurrence block for the chosen frequency onto *sched* and
|
"""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
|
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.inspector_id = form.inspector_id.data
|
||||||
sched.notes = (form.notes.data or '').strip() or None
|
sched.notes = (form.notes.data or '').strip() or None
|
||||||
sched.active = form.active.data
|
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)
|
_apply_recurrence(sched, form)
|
||||||
if _reject_if_past_end_date(sched, form):
|
if _reject_if_past_end_date(sched, form):
|
||||||
# sched is a persistent object and has already been mutated — discard
|
# 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))
|
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) ─────────────────────────────────
|
# ── Cron: reminders (advance / due / overdue) ─────────────────────────────────
|
||||||
|
|
||||||
@bp.route('/run', methods=['POST'])
|
@bp.route('/run', methods=['POST'])
|
||||||
|
|||||||
@@ -61,6 +61,16 @@
|
|||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
{# Start is shown only to the assignee — the inspection is theirs to do. #}
|
{# Start is shown only to the assignee — the inspection is theirs to do. #}
|
||||||
{% if s.inspector_id and s.inspector_id == current_user.id %}
|
{% 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) %}
|
{% set open_id = sched_open_inspections.get(s.id) %}
|
||||||
{% if open_id %}
|
{% if open_id %}
|
||||||
<a href="{{ url_for('inspections.execute', inspection_id=open_id) }}"
|
<a href="{{ url_for('inspections.execute', inspection_id=open_id) }}"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<th>Next Due</th>
|
<th>Next Due</th>
|
||||||
<th>Ends</th>
|
<th>Ends</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Confirmation</th>
|
||||||
<th>Created By</th>
|
<th>Created By</th>
|
||||||
<th class="text-end"></th>
|
<th class="text-end"></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -74,6 +75,31 @@
|
|||||||
<span class="badge bg-secondary">Inactive</span>
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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>
|
<td>
|
||||||
{{ s.creator.display_name if s.creator else '—' }}
|
{{ 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>
|
<div class="small text-muted">{{ s.created_at.strftime('%b %d, %Y') if s.created_at else '' }}</div>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""phase47 — scheduled inspection receipt acknowledgement
|
||||||
|
|
||||||
|
Adds to `scheduled_inspections`:
|
||||||
|
|
||||||
|
acknowledged_at DATETIME NULL
|
||||||
|
|
||||||
|
When the assigned inspector confirms they have received/seen a scheduled
|
||||||
|
inspection request. NULL = awaiting confirmation. Reset to NULL by the edit
|
||||||
|
route when the schedule is reassigned to a different inspector (once per
|
||||||
|
assignment, not per occurrence). The acknowledger is always the assigned
|
||||||
|
inspector, so no separate acknowledged_by column is stored.
|
||||||
|
|
||||||
|
No backfill: legacy rows keep NULL and render as "awaiting confirmation", which
|
||||||
|
is the correct initial state for an assignment nobody has confirmed yet.
|
||||||
|
|
||||||
|
Uses INFORMATION_SCHEMA check — safe to re-run.
|
||||||
|
"""
|
||||||
|
|
||||||
|
revision = 'phase47_sched_acknowledged'
|
||||||
|
down_revision = 'phase46_followup_req_by'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def _column_exists(conn, table, column):
|
||||||
|
return conn.execute(sa.text(
|
||||||
|
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS "
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() "
|
||||||
|
"AND TABLE_NAME = :t AND COLUMN_NAME = :c"
|
||||||
|
), {"t": table, "c": column}).scalar() > 0
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
if not _column_exists(bind, 'scheduled_inspections', 'acknowledged_at'):
|
||||||
|
op.execute(sa.text(
|
||||||
|
"ALTER TABLE scheduled_inspections ADD COLUMN acknowledged_at DATETIME NULL"
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
if _column_exists(bind, 'scheduled_inspections', 'acknowledged_at'):
|
||||||
|
op.execute(sa.text(
|
||||||
|
"ALTER TABLE scheduled_inspections DROP COLUMN acknowledged_at"
|
||||||
|
))
|
||||||
Reference in New Issue
Block a user