Jul 8 - Implement issue assignment separation
This commit is contained in:
@@ -245,9 +245,23 @@ issues: id, inspection_id (nullable), area_id, facility_id (nullable), severity
|
||||
mobile_local_id VARCHAR(64) nullable indexed, ← Phase B
|
||||
vendor_name VARCHAR(100) nullable, ← Phase 26
|
||||
vendor_contact VARCHAR(200) nullable, ← Phase 26
|
||||
vendor_notes TEXT nullable ← Phase 26
|
||||
vendor_notes TEXT nullable, ← Phase 26
|
||||
handler_type ENUM('internal','facility','vendor') NOT NULL DEFAULT 'internal', ← Phase 35
|
||||
facility_handler_name VARCHAR(100) nullable, ← Phase 35
|
||||
facility_handler_contact VARCHAR(200) nullable, ← Phase 35
|
||||
facility_handler_notes TEXT nullable ← Phase 35
|
||||
```
|
||||
|
||||
**Handler (`handler_type`, Phase 35) — who is doing the work:**
|
||||
|
||||
| Value | Meaning | Detail fields | `assigned_to` role |
|
||||
|---|---|---|---|
|
||||
| `internal` (default) | Our staff | — (the assignee IS the handler) | the handler |
|
||||
| `facility` | The facility's own staff | `facility_handler_name/contact/notes` (free text) | internal **follow-up owner** |
|
||||
| `vendor` | External contractor | `vendor_name/contact/notes` (Phase 26) | internal **follow-up owner** |
|
||||
|
||||
`assigned_to` (a JQC User) is **always** available: it is the handler for `internal`, and the internal follow-up owner (e.g. the inspector who verifies/updates) for `facility`/`vendor`. Set via the **Update Issue** panel on the issue detail page — the "Handled By" selector reveals the facility or vendor sub-fields via JS. Triage of `handler_type` + facility/vendor detail fields is **admin/director/project_manager only** (same gate as vendor fields); `assigned_to` remains admin/director. Issue list is filterable by `?handler_type=` and shows a Facility/Vendor badge. `Issue.handler_label` gives the display string. Not yet exposed in the mobile API.
|
||||
|
||||
**Photo columns — three distinct fields with different semantics:**
|
||||
|
||||
| Column | Type | Populated by | Displayed as |
|
||||
@@ -714,7 +728,8 @@ phase1_projects_roles → phase6_features → phase7_mobile_api → phase8_notif
|
||||
→ phase31_device_registry
|
||||
→ phase32_device_token_columns
|
||||
→ phase33_contract_recipients
|
||||
→ phase34_facility_qr ← HEAD
|
||||
→ phase34_facility_qr
|
||||
→ phase35_issue_handler ← HEAD
|
||||
```
|
||||
|
||||
### phase21_performance_indexes
|
||||
@@ -810,6 +825,16 @@ flask db upgrade # adds + backfills public_token
|
||||
sudo systemctl restart gunicorn
|
||||
```
|
||||
|
||||
### phase35_issue_handler
|
||||
|
||||
Revision id `phase35_issue_handler` (file `phase35_issue_handler_type.py`). Adds to `issues`: `handler_type ENUM('internal','facility','vendor') NOT NULL DEFAULT 'internal'` and `facility_handler_name/contact/notes`. **Backfills** existing rows with a non-empty `vendor_name` to `handler_type='vendor'`. Separates WHO handles an issue (see §5 Issue + the Handler section). `INFORMATION_SCHEMA` checks — safe to re-run.
|
||||
|
||||
**Deploy order:**
|
||||
```bash
|
||||
flask db upgrade
|
||||
sudo systemctl restart gunicorn
|
||||
```
|
||||
|
||||
**Deploy order for phases 24–32:**
|
||||
```bash
|
||||
flask db upgrade
|
||||
|
||||
@@ -84,6 +84,18 @@ class Issue(db.Model):
|
||||
vendor_contact = db.Column(db.String(200), nullable=True) # phone or email
|
||||
vendor_notes = db.Column(db.Text, nullable=True)
|
||||
|
||||
# ── Who handles the issue (phase35) ──────────────────────────────────
|
||||
# internal = our staff (assigned_to); facility = the facility's own staff
|
||||
# (facility_handler_* below); vendor = external contractor (vendor_* above).
|
||||
# assigned_to remains the internal follow-up owner in ALL cases.
|
||||
handler_type = db.Column(
|
||||
db.Enum('internal', 'facility', 'vendor'),
|
||||
nullable=False, default='internal',
|
||||
)
|
||||
facility_handler_name = db.Column(db.String(100), nullable=True)
|
||||
facility_handler_contact = db.Column(db.String(200), nullable=True) # phone or email
|
||||
facility_handler_notes = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Relationships
|
||||
# NOTE: Issue.area is provided by the backref on Area.issues (facility.py).
|
||||
# Do NOT add a second explicit db.relationship('Area') here — it conflicts
|
||||
@@ -102,6 +114,17 @@ class Issue(db.Model):
|
||||
"""Return True if the given user is currently following this issue."""
|
||||
return self.followers.filter_by(user_id=user.id).first() is not None
|
||||
|
||||
# Human-readable label for the handler category (phase35).
|
||||
HANDLER_LABELS = {
|
||||
'internal': 'Our Staff',
|
||||
'facility': 'Facility Staff',
|
||||
'vendor': 'External Vendor',
|
||||
}
|
||||
|
||||
@property
|
||||
def handler_label(self):
|
||||
return self.HANDLER_LABELS.get(self.handler_type or 'internal', 'Our Staff')
|
||||
|
||||
@property
|
||||
def resolved_facility(self):
|
||||
"""Returns the Facility for this issue regardless of which path was used to create it.
|
||||
|
||||
+19
-2
@@ -112,9 +112,12 @@ def export_list_pdf():
|
||||
date_from_filter = request.args.get('date_from', '')
|
||||
date_to_filter = request.args.get('date_to', '')
|
||||
reporter_filter = request.args.get('reporter_id', '')
|
||||
handler_filter = request.args.get('handler_type', '')
|
||||
|
||||
if issue_id_filter.isdigit():
|
||||
q = q.filter(Issue.id == int(issue_id_filter))
|
||||
if handler_filter in ('internal', 'facility', 'vendor'):
|
||||
q = q.filter(Issue.handler_type == handler_filter)
|
||||
if severity_filter:
|
||||
q = q.filter(Issue.severity == severity_filter)
|
||||
if status_filter:
|
||||
@@ -243,9 +246,12 @@ def index():
|
||||
date_from_filter = request.args.get('date_from', '')
|
||||
date_to_filter = request.args.get('date_to', '')
|
||||
reporter_filter = request.args.get('reporter_id', '')
|
||||
handler_filter = request.args.get('handler_type', '')
|
||||
|
||||
if issue_id_filter.isdigit():
|
||||
q = q.filter(Issue.id == int(issue_id_filter))
|
||||
if handler_filter in ('internal', 'facility', 'vendor'):
|
||||
q = q.filter(Issue.handler_type == handler_filter)
|
||||
if severity_filter:
|
||||
q = q.filter(Issue.severity == severity_filter)
|
||||
if status_filter:
|
||||
@@ -352,6 +358,7 @@ def index():
|
||||
date_from_filter=date_from_filter,
|
||||
date_to_filter=date_to_filter,
|
||||
reporter_filter=reporter_filter,
|
||||
handler_filter=handler_filter,
|
||||
facilities=facilities,
|
||||
projects=projects,
|
||||
staff=staff,
|
||||
@@ -432,12 +439,22 @@ def view(issue_id):
|
||||
|
||||
issue.result_notes = form.result_notes.data or None
|
||||
|
||||
# Vendor / contractor assignment — admin, director, project_manager only
|
||||
# Handler assignment (who handles it) + vendor/facility details —
|
||||
# admin, director, project_manager only.
|
||||
if current_user.role in ('admin', 'director', 'project_manager'):
|
||||
handler = form.handler_type.data or 'internal'
|
||||
if handler not in ('internal', 'facility', 'vendor'):
|
||||
handler = 'internal'
|
||||
issue.handler_type = handler
|
||||
|
||||
issue.vendor_name = (form.vendor_name.data or '').strip() or None
|
||||
issue.vendor_contact = (form.vendor_contact.data or '').strip() or None
|
||||
issue.vendor_notes = (form.vendor_notes.data or '').strip() or None
|
||||
|
||||
issue.facility_handler_name = (form.facility_handler_name.data or '').strip() or None
|
||||
issue.facility_handler_contact = (form.facility_handler_contact.data or '').strip() or None
|
||||
issue.facility_handler_notes = (form.facility_handler_notes.data or '').strip() or None
|
||||
|
||||
from app.routes.inspections import _save_photo
|
||||
new_photos = []
|
||||
for file_obj in request.files.getlist('result_photos'):
|
||||
@@ -595,7 +612,7 @@ def view(issue_id):
|
||||
db.session.commit() # Single atomic commit: issue fields + comment + all notification rows
|
||||
log_action(ACTION_UPDATE, 'Issue', issue.id,
|
||||
f'#{issue.id} in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'}',
|
||||
f'status={issue.status}; assigned_to={issue.assigned_to}')
|
||||
f'status={issue.status}; handler={issue.handler_type}; assigned_to={issue.assigned_to}')
|
||||
flash('Issue updated.', 'success')
|
||||
return redirect(url_for('issues.view', issue_id=issue_id))
|
||||
|
||||
|
||||
@@ -82,6 +82,15 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small mb-1">Handled By</label>
|
||||
<select name="handler_type" class="form-select form-select-sm">
|
||||
<option value="">All Handlers</option>
|
||||
<option value="internal" {{ 'selected' if handler_filter == 'internal' }}>Our Staff</option>
|
||||
<option value="facility" {{ 'selected' if handler_filter == 'facility' }}>Facility Staff</option>
|
||||
<option value="vendor" {{ 'selected' if handler_filter == 'vendor' }}>External Vendor</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto d-flex align-items-end gap-2 flex-wrap">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">Filter</button>
|
||||
<a href="{{ url_for('issues.index') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
|
||||
@@ -172,6 +181,11 @@
|
||||
{% if issue.assigned_user %}{{ issue.assigned_user.display_name }}
|
||||
{% else %}<span class="text-muted">—</span>{% endif %}
|
||||
{% endif %}
|
||||
{% if issue.handler_type == 'facility' %}
|
||||
<div><span class="badge bg-info text-dark mt-1" title="Handled by facility staff"><i class="bi bi-building"></i> Facility</span></div>
|
||||
{% elif issue.handler_type == 'vendor' %}
|
||||
<div><span class="badge bg-warning text-dark mt-1" title="Handled by external vendor"><i class="bi bi-person-gear"></i> Vendor</span></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{# Following badge + inline unfollow #}
|
||||
@@ -184,7 +198,7 @@
|
||||
class="d-inline"
|
||||
title="Unfollow this issue">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="next" value="{{ url_for('issues.index', page=issues.page, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter, reporter_id=reporter_filter) }}">
|
||||
<input type="hidden" name="next" value="{{ url_for('issues.index', page=issues.page, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter, reporter_id=reporter_filter, handler_type=handler_filter) }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary p-0 px-1 me-1"
|
||||
title="Unfollow">
|
||||
<i class="bi bi-bell-slash" style="font-size:.75rem;"></i>
|
||||
@@ -225,7 +239,7 @@
|
||||
{% if p %}
|
||||
<li class="page-item {{ 'active' if p == issues.page }}">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('issues.index', page=p, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, sla=sla_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter, reporter_id=reporter_filter) }}">{{ p }}</a>
|
||||
href="{{ url_for('issues.index', page=p, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, sla=sla_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter, reporter_id=reporter_filter, handler_type=handler_filter) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}<li class="page-item disabled"><span class="page-link">…</span></li>{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
+123
-28
@@ -73,10 +73,37 @@
|
||||
</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-sm-3">Assigned To</dt>
|
||||
<dt class="col-sm-3">Handled By</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% if issue.handler_type == 'facility' %}
|
||||
<span class="badge bg-info text-dark"><i class="bi bi-building me-1"></i>Facility Staff</span>
|
||||
{% elif issue.handler_type == 'vendor' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-person-gear me-1"></i>External Vendor</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary"><i class="bi bi-people me-1"></i>Our Staff</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">
|
||||
{{ 'Follow-up Owner' if issue.handler_type in ['facility','vendor'] else 'Assigned To' }}
|
||||
</dt>
|
||||
<dd class="col-sm-9">{{ issue.assigned_user.display_name if issue.assigned_user else '— Unassigned —' }}</dd>
|
||||
|
||||
{% if issue.vendor_name %}
|
||||
{% if issue.handler_type == 'facility' and issue.facility_handler_name %}
|
||||
<dt class="col-sm-3">Facility Contact</dt>
|
||||
<dd class="col-sm-9">
|
||||
<i class="bi bi-building text-secondary me-1"></i>
|
||||
<strong>{{ issue.facility_handler_name }}</strong>
|
||||
{% if issue.facility_handler_contact %}
|
||||
<span class="text-muted ms-2">{{ issue.facility_handler_contact }}</span>
|
||||
{% endif %}
|
||||
{% if issue.facility_handler_notes %}
|
||||
<div class="text-muted small mt-1" style="white-space:pre-wrap;">{{ issue.facility_handler_notes }}</div>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if issue.handler_type == 'vendor' and issue.vendor_name %}
|
||||
<dt class="col-sm-3">Contractor</dt>
|
||||
<dd class="col-sm-9">
|
||||
<i class="bi bi-person-gear text-secondary me-1"></i>
|
||||
@@ -341,12 +368,77 @@
|
||||
{{ form.status.label(class="form-label fw-semibold") }}
|
||||
{{ form.status(class="form-select") }}
|
||||
</div>
|
||||
{% if current_user.role in ['admin','director'] %}
|
||||
{% if current_user.role in ['admin','director','project_manager'] %}
|
||||
{# ── Who handles this issue ── #}
|
||||
<div class="mb-3">
|
||||
{{ form.assigned_to.label(class="form-label fw-semibold") }}
|
||||
{{ form.assigned_to(class="form-select") }}
|
||||
{{ form.handler_type.label(class="form-label fw-semibold") }}
|
||||
{{ form.handler_type(class="form-select", id="handler_type_select") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.role in ['admin','director'] %}
|
||||
<div class="mb-3" id="assigned_to_wrap">
|
||||
<label class="form-label fw-semibold" id="assigned_to_label">Assign To</label>
|
||||
{{ form.assigned_to(class="form-select") }}
|
||||
<div class="form-text" id="assigned_to_help" style="display:none;">
|
||||
The facility/vendor does the work; this is our follow-up owner.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.role in ['admin','director','project_manager'] %}
|
||||
{# ── Facility-staff handler (shown when Handled By = Facility Staff) ── #}
|
||||
<div id="facility_handler_block" style="display:none;">
|
||||
<hr class="my-3">
|
||||
<p class="fw-semibold small mb-2">
|
||||
<i class="bi bi-building me-1 text-secondary"></i>Facility Staff Contact
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
{{ form.facility_handler_name.label(class="form-label small fw-semibold mb-1") }}
|
||||
{{ form.facility_handler_name(class="form-control form-control-sm",
|
||||
placeholder="Facility staff / point of contact",
|
||||
value=issue.facility_handler_name or '') }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
{{ form.facility_handler_contact.label(class="form-label small fw-semibold mb-1") }}
|
||||
{{ form.facility_handler_contact(class="form-control form-control-sm",
|
||||
placeholder="Phone or email",
|
||||
value=issue.facility_handler_contact or '') }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.facility_handler_notes.label(class="form-label small fw-semibold mb-1") }}
|
||||
{{ form.facility_handler_notes(class="form-control form-control-sm", rows=2,
|
||||
placeholder="What the facility staff are handling…") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── External vendor (shown when Handled By = External Vendor) ── #}
|
||||
<div id="vendor_block" style="display:none;">
|
||||
<hr class="my-3">
|
||||
<p class="fw-semibold small mb-2">
|
||||
<i class="bi bi-person-gear me-1 text-secondary"></i>External Contractor
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
{{ form.vendor_name.label(class="form-label small fw-semibold mb-1") }}
|
||||
{{ form.vendor_name(class="form-control form-control-sm",
|
||||
placeholder="Contractor or vendor name",
|
||||
value=issue.vendor_name or '') }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
{{ form.vendor_contact.label(class="form-label small fw-semibold mb-1") }}
|
||||
{{ form.vendor_contact(class="form-control form-control-sm",
|
||||
placeholder="Phone or email",
|
||||
value=issue.vendor_contact or '') }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.vendor_notes.label(class="form-label small fw-semibold mb-1") }}
|
||||
{{ form.vendor_notes(class="form-control form-control-sm", rows=2,
|
||||
placeholder="Notes about what the contractor is handling…") }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr class="my-3">
|
||||
<div class="mb-3">
|
||||
{{ form.result_notes.label(class="form-label fw-semibold") }}
|
||||
{{ form.result_notes(class="form-control", rows=3,
|
||||
@@ -364,29 +456,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if current_user.role in ['admin','director','project_manager'] %}
|
||||
<hr class="my-3">
|
||||
<p class="fw-semibold small mb-2">
|
||||
<i class="bi bi-person-gear me-1 text-secondary"></i>External Contractor
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
{{ form.vendor_name.label(class="form-label small fw-semibold mb-1") }}
|
||||
{{ form.vendor_name(class="form-control form-control-sm",
|
||||
placeholder="Contractor or vendor name",
|
||||
value=issue.vendor_name or '') }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
{{ form.vendor_contact.label(class="form-label small fw-semibold mb-1") }}
|
||||
{{ form.vendor_contact(class="form-control form-control-sm",
|
||||
placeholder="Phone or email",
|
||||
value=issue.vendor_contact or '') }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.vendor_notes.label(class="form-label small fw-semibold mb-1") }}
|
||||
{{ form.vendor_notes(class="form-control form-control-sm", rows=2,
|
||||
placeholder="Notes about what the contractor is handling…") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary w-100">Save Update</button>
|
||||
</form>
|
||||
{% if issue.status in ['in_progress', 'resolved'] and current_user.role not in ['customer'] %}
|
||||
@@ -476,6 +545,32 @@
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
// ── "Handled By" — show the relevant sub-block (facility vs vendor) and
|
||||
// relabel the assignee as a follow-up owner for facility/vendor. ──
|
||||
var handlerSelect = document.getElementById('handler_type_select');
|
||||
function syncHandlerUI() {
|
||||
if (!handlerSelect) { return; }
|
||||
var v = handlerSelect.value;
|
||||
var facBlock = document.getElementById('facility_handler_block');
|
||||
var venBlock = document.getElementById('vendor_block');
|
||||
if (facBlock) { facBlock.style.display = (v === 'facility') ? '' : 'none'; }
|
||||
if (venBlock) { venBlock.style.display = (v === 'vendor') ? '' : 'none'; }
|
||||
|
||||
var label = document.getElementById('assigned_to_label');
|
||||
var help = document.getElementById('assigned_to_help');
|
||||
if (label) {
|
||||
label.textContent = (v === 'facility' || v === 'vendor')
|
||||
? 'Follow-up Owner' : 'Assign To';
|
||||
}
|
||||
if (help) {
|
||||
help.style.display = (v === 'facility' || v === 'vendor') ? '' : 'none';
|
||||
}
|
||||
}
|
||||
if (handlerSelect) {
|
||||
handlerSelect.addEventListener('change', syncHandlerUI);
|
||||
syncHandlerUI(); // set initial state on load
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
+11
-1
@@ -185,7 +185,17 @@ class IssueUpdateForm(FlaskForm):
|
||||
Optional(),
|
||||
FileAllowed(['jpg','jpeg','png','gif'], 'Images only.')
|
||||
])
|
||||
# External contractor / vendor fields (phase26)
|
||||
# Who handles the issue (phase35)
|
||||
handler_type = SelectField('Handled By', choices=[
|
||||
('internal', 'Our Staff'),
|
||||
('facility', 'Facility Staff'),
|
||||
('vendor', 'External Vendor'),
|
||||
], validators=[Optional()])
|
||||
# Facility-staff handler (free text) — used when handler_type == 'facility'
|
||||
facility_handler_name = StringField('Facility Contact Name', validators=[Optional(), Length(max=100)])
|
||||
facility_handler_contact = StringField('Facility Contact', validators=[Optional(), Length(max=200)])
|
||||
facility_handler_notes = TextAreaField('Facility Handling Notes', validators=[Optional(), Length(max=1000)])
|
||||
# External contractor / vendor fields (phase26) — used when handler_type == 'vendor'
|
||||
vendor_name = StringField('Contractor Name', validators=[Optional(), Length(max=100)])
|
||||
vendor_contact = StringField('Contractor Contact', validators=[Optional(), Length(max=200)])
|
||||
vendor_notes = TextAreaField('Contractor Notes', validators=[Optional(), Length(max=1000)])
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""phase35 — issue handler_type + facility-staff handler fields
|
||||
|
||||
Separates WHO handles an issue into three categories:
|
||||
internal — one of our staff (existing assigned_to User)
|
||||
facility — the facility's own staff (new free-text facility_handler_* fields)
|
||||
vendor — an external contractor (existing vendor_* fields, phase26)
|
||||
|
||||
`assigned_to` remains the internal follow-up owner in all cases.
|
||||
|
||||
Backfills existing rows that already have a vendor_name to handler_type='vendor'.
|
||||
Uses INFORMATION_SCHEMA column-existence checks — safe to re-run.
|
||||
"""
|
||||
|
||||
revision = 'phase35_issue_handler'
|
||||
down_revision = 'phase34_facility_qr'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def _column_exists(conn, table, column):
|
||||
result = 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})
|
||||
return result.scalar() > 0
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
|
||||
if not _column_exists(bind, 'issues', 'handler_type'):
|
||||
op.execute(sa.text(
|
||||
"ALTER TABLE issues ADD COLUMN handler_type "
|
||||
"ENUM('internal','facility','vendor') NOT NULL DEFAULT 'internal'"
|
||||
))
|
||||
|
||||
if not _column_exists(bind, 'issues', 'facility_handler_name'):
|
||||
op.execute(sa.text(
|
||||
"ALTER TABLE issues ADD COLUMN facility_handler_name VARCHAR(100) NULL"
|
||||
))
|
||||
if not _column_exists(bind, 'issues', 'facility_handler_contact'):
|
||||
op.execute(sa.text(
|
||||
"ALTER TABLE issues ADD COLUMN facility_handler_contact VARCHAR(200) NULL"
|
||||
))
|
||||
if not _column_exists(bind, 'issues', 'facility_handler_notes'):
|
||||
op.execute(sa.text(
|
||||
"ALTER TABLE issues ADD COLUMN facility_handler_notes TEXT NULL"
|
||||
))
|
||||
|
||||
# Backfill: rows already carrying a vendor become handler_type='vendor'
|
||||
# so existing contractor assignments keep their meaning.
|
||||
op.execute(sa.text(
|
||||
"UPDATE issues SET handler_type = 'vendor' "
|
||||
"WHERE handler_type = 'internal' "
|
||||
"AND vendor_name IS NOT NULL AND TRIM(vendor_name) <> ''"
|
||||
))
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
for col in ('facility_handler_notes', 'facility_handler_contact',
|
||||
'facility_handler_name', 'handler_type'):
|
||||
if _column_exists(bind, 'issues', col):
|
||||
op.execute(sa.text(f"ALTER TABLE issues DROP COLUMN {col}"))
|
||||
Reference in New Issue
Block a user