05/11 Update: add contract & facility import function

This commit is contained in:
2026-05-11 13:05:40 -04:00
parent dc9bfb1674
commit b73448dfd0
3 changed files with 605 additions and 2 deletions
+168
View File
@@ -0,0 +1,168 @@
{% extends "base.html" %}
{% block title %}Import Contracts & Facilities{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2><i class="bi bi-file-earmark-excel text-success me-2"></i>Import Contracts &amp; Facilities</h2>
<p class="text-muted mb-0">Bulk-create contracts and their facilities from an Excel workbook.</p>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('projects.import_template') }}" class="btn btn-sm btn-outline-success">
<i class="bi bi-download me-1"></i>Download Template
</a>
<a href="{{ url_for('projects.index') }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Contracts
</a>
</div>
</div>
{# ── Format guide ── #}
<div class="card border-0 bg-light mb-4">
<div class="card-body py-3 px-4">
<h6 class="fw-semibold mb-2"><i class="bi bi-info-circle me-1 text-primary"></i>Excel Format — two sheets</h6>
<div class="row g-3 small">
<div class="col-md-4">
<strong>Sheet: <code>Contracts</code></strong>
<ul class="mb-0 mt-1 ps-3">
<li><code>contract_name</code> <span class="text-danger">*required</span></li>
<li><code>description</code> — optional</li>
<li><code>active</code> — yes/no (default: yes)</li>
</ul>
</div>
<div class="col-md-4">
<strong>Sheet: <code>Facilities</code></strong>
<ul class="mb-0 mt-1 ps-3">
<li><code>contract_name</code> <span class="text-danger">*required</span> — must match a row in Contracts sheet</li>
<li><code>facility_name</code> <span class="text-danger">*required</span></li>
<li><code>address</code>, <code>contact_person</code>, <code>contact_phone</code> — optional</li>
<li><code>active</code> — yes/no (default: yes)</li>
</ul>
</div>
<div class="col-md-4">
<strong>Tips</strong>
<ul class="mb-0 mt-1 ps-3">
<li>Contracts that already exist (by name) are reused — not duplicated</li>
<li>Facilities that already exist within the same contract are skipped</li>
<li>The Facilities sheet is optional — you can import contracts only</li>
<li>Download the template to see the expected structure</li>
</ul>
</div>
</div>
</div>
</div>
{# ── Upload form (Phase 1) ── #}
{% if not preview_rows %}
<div class="card shadow-sm">
<div class="card-header bg-success text-white fw-semibold">
<i class="bi bi-file-earmark-arrow-up me-1"></i> Upload Excel File
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label fw-semibold">Select .xlsx File</label>
<input type="file" name="xlsx_file" accept=".xlsx,.xlsm" class="form-control" required>
<div class="form-text">Maximum recommended file size: 2 MB.</div>
</div>
<button type="submit" class="btn btn-success">
<i class="bi bi-search me-1"></i> Parse &amp; Preview
</button>
</form>
</div>
</div>
{% else %}
{# ── Preview results (Phase 1 response) ── #}
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center
bg-{{ 'danger' if has_errors else 'success' }} text-white">
<span class="fw-semibold">
<i class="bi bi-{{ 'x-circle' if has_errors else 'check-circle' }} me-1"></i>
Preview — {{ preview_rows|length }} row(s) parsed
</span>
<span>
<span class="badge bg-white text-success">{{ valid_count }} ready</span>
{% set exists_count = preview_rows | selectattr('status', 'equalto', 'exists') | list | length %}
{% if exists_count > 0 %}
<span class="badge bg-white text-warning ms-1">{{ exists_count }} existing</span>
{% endif %}
{% set err_count = preview_rows | selectattr('status', 'equalto', 'error') | list | length %}
{% if err_count > 0 %}
<span class="badge bg-white text-danger ms-1">{{ err_count }} error(s)</span>
{% endif %}
</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0" style="font-size:.85rem;">
<thead class="table-light">
<tr>
<th width="50">Row</th>
<th width="100">Sheet</th>
<th>Contract Name</th>
<th>Facility Name</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for row in preview_rows %}
<tr class="{{ 'table-danger' if row.status == 'error' else ('table-warning' if row.status == 'exists' else '') }}">
<td class="text-muted">{{ row.row }}</td>
<td><span class="badge bg-secondary">{{ row.sheet }}</span></td>
<td>{{ row.contract_name or '—' }}</td>
<td>{{ row.facility_name or '—' }}</td>
<td>
{% if row.status == 'ok' %}
<span class="badge bg-success">Ready</span>
{% elif row.status == 'exists' %}
<span class="badge bg-warning text-dark">Exists</span>
<span class="text-muted ms-1" style="font-size:.78rem;">{{ row.note }}</span>
{% else %}
<span class="badge bg-danger">Error</span>
<ul class="mb-0 ps-3 text-danger" style="font-size:.78rem;">
{% for e in row.errors %}<li>{{ e }}</li>{% endfor %}
</ul>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# ── Action buttons (Phase 2 trigger) ── #}
{% if has_errors %}
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle-fill me-1"></i>
<strong>Errors found.</strong> Fix the issues above and re-upload.
{% if valid_count > 0 %}
You may still import the {{ valid_count }} valid row(s) by clicking below.
{% endif %}
</div>
{% endif %}
<div class="d-flex gap-3 align-items-center">
{% if valid_count > 0 %}
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="confirmed" value="1">
<input type="hidden" name="rows_json" value="{{ rows_json }}">
<button type="submit" class="btn btn-success"
onclick="return confirm('Proceed with importing {{ valid_count }} valid row(s)?')">
<i class="bi bi-check-circle me-1"></i>
Import {{ valid_count }} Valid Row{{ 's' if valid_count != 1 else '' }}
</button>
</form>
{% endif %}
<a href="{{ url_for('projects.bulk_import') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-counterclockwise me-1"></i> Upload Different File
</a>
</div>
{% endif %}
{% endblock %}
+4 -1
View File
@@ -7,7 +7,10 @@
<h2><i class="bi bi-folder2-open"></i> Contracts</h2>
</div>
{% if current_user.role in ['admin', 'director'] %}
<div class="col-auto">
<div class="col-auto d-flex gap-2">
<a href="{{ url_for('projects.bulk_import') }}" class="btn btn-outline-success">
<i class="bi bi-file-earmark-excel"></i> Import
</a>
<a href="{{ url_for('projects.create') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> New Contract
</a>