345 lines
17 KiB
HTML
345 lines
17 KiB
HTML
{% 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 %}
|
|
|
|
{# ── Additional Notification Recipients ── #}
|
|
{% 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-bell"></i> Additional Notification Recipients
|
|
</span>
|
|
<button class="btn btn-sm btn-primary" type="button"
|
|
data-bs-toggle="collapse" data-bs-target="#addNotifyRecipient">
|
|
<i class="bi bi-plus-lg"></i> Add Recipient
|
|
</button>
|
|
</div>
|
|
|
|
{# Add form (collapsed by default) #}
|
|
<div class="collapse border-bottom" id="addNotifyRecipient">
|
|
<form method="POST"
|
|
action="{{ url_for('projects.add_notify_recipient', project_id=project.id) }}"
|
|
class="p-3">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
|
|
<div class="row g-3 align-items-start">
|
|
{# Recipient type + target #}
|
|
<div class="col-md-5">
|
|
<label class="form-label small fw-semibold">Recipient</label>
|
|
<div class="btn-group btn-group-sm w-100 mb-2" role="group">
|
|
<input type="radio" class="btn-check" name="recipient_type"
|
|
id="rtypeUser" value="user" checked
|
|
onchange="toggleRecipientType()">
|
|
<label class="btn btn-outline-secondary" for="rtypeUser">Staff User</label>
|
|
<input type="radio" class="btn-check" name="recipient_type"
|
|
id="rtypeEmail" value="email"
|
|
onchange="toggleRecipientType()">
|
|
<label class="btn btn-outline-secondary" for="rtypeEmail">Email Address</label>
|
|
</div>
|
|
|
|
<div id="recipientUserWrap">
|
|
<select name="user_id" class="form-select form-select-sm">
|
|
<option value="">— Select a staff user —</option>
|
|
{% for u in staff_users %}
|
|
<option value="{{ u.id }}">{{ u.display_name }} ({{ u.role }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div id="recipientEmailWrap" class="d-none">
|
|
<input type="email" name="email" class="form-control form-control-sm"
|
|
placeholder="name@example.com">
|
|
<div class="form-text">External address — receives email only.</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# Event subscription #}
|
|
<div class="col-md-7">
|
|
<label class="form-label small fw-semibold d-flex justify-content-between">
|
|
<span>Notify for events</span>
|
|
<span>
|
|
<a href="#" class="small text-decoration-none"
|
|
onclick="setAllEvents(true);return false;">All</a> /
|
|
<a href="#" class="small text-decoration-none"
|
|
onclick="setAllEvents(false);return false;">None</a>
|
|
</span>
|
|
</label>
|
|
<div class="row row-cols-1 row-cols-lg-2 g-1"
|
|
style="max-height:180px;overflow-y:auto;">
|
|
{% for ekey, elabel in matrix_events.items() %}
|
|
<div class="col">
|
|
<div class="form-check">
|
|
<input class="form-check-input notify-event-cb" type="checkbox"
|
|
name="event_types" value="{{ ekey }}"
|
|
id="ev_{{ ekey }}">
|
|
<label class="form-check-label small" for="ev_{{ ekey }}">
|
|
{{ elabel }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3 text-end">
|
|
<button type="submit" class="btn btn-sm btn-success">
|
|
<i class="bi bi-check-lg"></i> Save Recipient
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{# Existing recipients #}
|
|
<div class="card-body p-0">
|
|
{% if notify_recipients %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 align-middle">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Recipient</th>
|
|
<th>Type</th>
|
|
<th>Notified Events</th>
|
|
<th width="80"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in notify_recipients %}
|
|
<tr>
|
|
<td>
|
|
{% if r.user_id %}
|
|
<strong>{{ r.user.display_name if r.user else '—' }}</strong>
|
|
<div class="text-muted small">{{ r.user.email if r.user else '' }}</div>
|
|
{% else %}
|
|
<strong>{{ r.email }}</strong>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if r.user_id %}
|
|
<span class="badge bg-primary">Staff · in-app + email</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">External · email</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% set ev = r.get_event_types() %}
|
|
{% if ev %}
|
|
<div class="d-flex flex-wrap gap-1">
|
|
{% for e in ev %}
|
|
<span class="badge bg-light text-dark border">
|
|
{{ matrix_events.get(e, e) }}
|
|
</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted small">None</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<form method="POST"
|
|
action="{{ url_for('projects.remove_notify_recipient', recipient_id=r.id) }}"
|
|
onsubmit="return confirm('Remove this notification recipient?');">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-sm btn-outline-danger"
|
|
title="Remove recipient">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="p-3 text-muted small">
|
|
No additional recipients. These are notified — for the events you pick —
|
|
on top of the global
|
|
<a href="{{ url_for('auth.notification_matrix') }}">Notification Matrix</a>.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
{% if current_user.role == 'admin' %}
|
|
<script>
|
|
function toggleRecipientType() {
|
|
var isUser = document.getElementById('rtypeUser').checked;
|
|
document.getElementById('recipientUserWrap').classList.toggle('d-none', !isUser);
|
|
document.getElementById('recipientEmailWrap').classList.toggle('d-none', isUser);
|
|
}
|
|
function setAllEvents(state) {
|
|
document.querySelectorAll('.notify-event-cb').forEach(function (cb) { cb.checked = state; });
|
|
}
|
|
</script>
|
|
{% endif %}
|
|
{% endblock %}
|