First commit
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-person-plus"></i> {{ title }}
|
||||
</h4>
|
||||
<small class="text-white-50">Contract: {{ project.name }}</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.user_id.label(class="form-label") }}
|
||||
{{ form.user_id(class="form-select") }}
|
||||
{% if form.user_id.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{% for e in form.user_id.errors %}{{ e }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Only active users with the Customer role are listed.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.facility_id.label(class="form-label") }}
|
||||
{{ form.facility_id(class="form-select") }}
|
||||
<div class="form-text">
|
||||
Select "All facilities in contract" to grant access to every facility
|
||||
within this contract, or choose a specific facility to restrict access.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-person-check"></i> Assign Customer
|
||||
</button>
|
||||
<a href="{{ url_for('projects.view', project_id=project.id) }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0">{{ title }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.name.label(class="form-label") }}
|
||||
{{ form.name(class="form-control") }}
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{% for e in form.name.errors %}{{ e }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.description.label(class="form-label") }}
|
||||
{{ form.description(class="form-control", rows=3) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.project_manager_id.label(class="form-label") }}
|
||||
{{ form.project_manager_id(class="form-select") }}
|
||||
<div class="form-text">Only users with the Project Manager role are listed.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check">
|
||||
{{ form.active(class="form-check-input") }}
|
||||
{{ form.active.label(class="form-check-label") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Save Contract
|
||||
</button>
|
||||
<a href="{{ url_for('projects.index') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 & 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 & 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 %}
|
||||
@@ -0,0 +1,84 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Contracts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col">
|
||||
<h2><i class="bi bi-folder2-open"></i> Contracts</h2>
|
||||
</div>
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{% for project in projects %}
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="card-title mb-0">
|
||||
<a href="{{ url_for('projects.view', project_id=project.id) }}" class="text-decoration-none">
|
||||
{{ project.name }}
|
||||
</a>
|
||||
</h5>
|
||||
{% if not project.active %}
|
||||
<span class="badge bg-secondary ms-2">Inactive</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success ms-2">Active</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if project.description %}
|
||||
<p class="card-text text-muted small">{{ project.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3 d-flex gap-3">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-building"></i> {{ project.facilities.count() }} facilities
|
||||
</small>
|
||||
{% if project.project_manager %}
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-person-badge"></i> {{ project.project_manager.display_name }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent d-flex gap-2">
|
||||
<a href="{{ url_for('projects.view', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</a>
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('projects.edit', project_id=project.id) }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.role == 'admin' %}
|
||||
<form method="POST" action="{{ url_for('projects.delete', project_id=project.id) }}"
|
||||
class="ms-auto d-inline"
|
||||
onsubmit="return confirm('Delete contract \'{{ project.name }}\'? This cannot be undone.');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> No contracts have been created yet.
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,169 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ project.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col">
|
||||
<h2>
|
||||
<i class="bi bi-folder2-open"></i> {{ project.name }}
|
||||
{% if not project.active %}
|
||||
<span class="badge bg-secondary ms-2 fs-6">Inactive</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% if project.description %}
|
||||
<p class="text-muted">{{ project.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-auto d-flex gap-2">
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('projects.edit', project_id=project.id) }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('projects.index') }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-arrow-left"></i> All Contracts
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{# ── Contract Info ── #}
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light fw-semibold">
|
||||
<i class="bi bi-info-circle"></i> Contract Details
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-5 text-muted small">Status</dt>
|
||||
<dd class="col-7">
|
||||
<span class="badge bg-{{ 'success' if project.active else 'secondary' }}">
|
||||
{{ 'Active' if project.active else 'Inactive' }}
|
||||
</span>
|
||||
</dd>
|
||||
<dt class="col-5 text-muted small">Contract Manager</dt>
|
||||
<dd class="col-7 small">
|
||||
{{ project.project_manager.display_name if project.project_manager else '—' }}
|
||||
</dd>
|
||||
<dt class="col-5 text-muted small">Created</dt>
|
||||
<dd class="col-7 small">{{ project.created_at.strftime('%Y-%m-%d') }}</dd>
|
||||
<dt class="col-5 text-muted small">Facilities</dt>
|
||||
<dd class="col-7 small">{{ facilities|length }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Facilities ── #}
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold"><i class="bi bi-building"></i> Facilities</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if facilities %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in facilities %}
|
||||
<tr>
|
||||
<td><strong>{{ f.name }}</strong></td>
|
||||
<td class="text-muted small">{{ f.address or '—' }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ 'success' if f.active else 'secondary' }}">
|
||||
{{ 'Active' if f.active else 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('facilities.view_facility', facility_id=f.id) }}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-3 text-muted small">No facilities linked to this contract yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Customer Assignments ── #}
|
||||
{% if current_user.role == 'admin' %}
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold"><i class="bi bi-people"></i> Customer Assignments</span>
|
||||
<a href="{{ url_for('projects.add_assignment', project_id=project.id) }}"
|
||||
class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-person-plus"></i> Add Customer
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if assignments %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Customer Username</th>
|
||||
<th>Email</th>
|
||||
<th>Facility Scope</th>
|
||||
<th>Assigned</th>
|
||||
<th width="80"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in assignments %}
|
||||
<tr>
|
||||
<td><strong>{{ a.user.username }}</strong></td>
|
||||
<td class="text-muted small">{{ a.user.email }}</td>
|
||||
<td>
|
||||
{% if a.facility %}
|
||||
<span class="badge bg-info text-dark">{{ a.facility.name }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">All facilities</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="small text-muted">{{ a.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
<form method="POST"
|
||||
action="{{ url_for('projects.remove_assignment', assignment_id=a.id) }}"
|
||||
onsubmit="return confirm('Remove assignment for {{ a.user.username }}?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
title="Remove assignment">
|
||||
<i class="bi bi-person-dash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-3 text-muted small">
|
||||
No customer users assigned yet.
|
||||
<a href="{{ url_for('projects.add_assignment', project_id=project.id) }}">Add one now.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user