161 lines
6.0 KiB
HTML
161 lines
6.0 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Bulk Customer Import{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h2><i class="bi bi-upload text-primary me-2"></i>Bulk Customer Import</h2>
|
|
<p class="text-muted mb-0">Create multiple customer accounts and assignments from a CSV file.</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="{{ url_for('customers.import_template') }}" class="btn btn-sm btn-outline-success">
|
|
<i class="bi bi-download me-1"></i>Download Template
|
|
</a>
|
|
<a href="{{ url_for('customers.index') }}" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-arrow-left me-1"></i>Back to Customers
|
|
</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>CSV Format</h6>
|
|
<div class="row g-3 small">
|
|
<div class="col-md-4">
|
|
<strong>Required columns</strong>
|
|
<ul class="mb-0 mt-1 ps-3">
|
|
<li><code>username</code> — unique login name</li>
|
|
<li><code>email</code> — unique email address</li>
|
|
<li><code>password</code> — min 8 characters</li>
|
|
</ul>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<strong>Optional columns</strong>
|
|
<ul class="mb-0 mt-1 ps-3">
|
|
<li><code>project_name</code> — exact contract name</li>
|
|
<li><code>facility_name</code> — exact facility name within contract (leave blank for all)</li>
|
|
</ul>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<strong>Tips</strong>
|
|
<ul class="mb-0 mt-1 ps-3">
|
|
<li>Repeat a username on multiple rows to assign them to multiple contracts</li>
|
|
<li>Leave <code>facility_name</code> blank to grant access to all facilities in the contract</li>
|
|
<li>Existing usernames/emails will be flagged as errors before anything is saved</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Upload form ── #}
|
|
{% if not preview_rows %}
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-primary text-white fw-semibold">
|
|
<i class="bi bi-file-earmark-arrow-up me-1"></i> Upload CSV 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 CSV File</label>
|
|
<input type="file" name="csv_file" accept=".csv" class="form-control" required>
|
|
<div class="form-text">Maximum recommended file size: 500 KB · UTF-8 encoding.</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-search me-1"></i> Parse & Preview
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{% else %}
|
|
{# ── Preview results ── #}
|
|
<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 }} valid</span>
|
|
{% set err_count = preview_rows|length - valid_count %}
|
|
{% 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>Username</th>
|
|
<th>Email</th>
|
|
<th>Contract</th>
|
|
<th>Facility Scope</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in preview_rows %}
|
|
<tr class="{{ 'table-danger' if row.status == 'error' else '' }}">
|
|
<td class="text-muted">{{ row.row }}</td>
|
|
<td>{{ row.username or '—' }}</td>
|
|
<td>{{ row.email or '—' }}</td>
|
|
<td>{{ row.project }}</td>
|
|
<td>{{ row.facility }}</td>
|
|
<td>
|
|
{% if row.status == 'ok' %}
|
|
<span class="badge bg-success">Ready</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 ── #}
|
|
{% 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.
|
|
Error rows will be skipped — only valid rows can be imported.
|
|
{% 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('Create {{ valid_count }} customer account(s) and their assignments?')">
|
|
<i class="bi bi-person-check me-1"></i>
|
|
Import {{ valid_count }} Valid Row{{ 's' if valid_count != 1 else '' }}
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
<a href="{{ url_for('customers.bulk_import') }}" class="btn btn-outline-secondary">
|
|
<i class="bi bi-arrow-counterclockwise me-1"></i> Upload Different File
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|