Files
QWE-Telco/TelcoPortal-source/templates/phones.html
T

361 lines
17 KiB
HTML
Executable File

{% extends "base.html" %}
{% block title %}Mobile Phones - Queensway Europe Telco Portal{% endblock %}
{% block content %}
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2">
<h2 class="mb-0 d-flex align-items-center gap-2">
{% include "icons/phone.svg" %} Mobile Phones
<span class="carrier-badge" title="Carrier">Orange</span>
</h2>
{% if session.role == 'admin' %}
<button type="button" class="btn btn-success d-inline-flex align-items-center gap-2" onclick="openPhoneModal(null)">
{% include "icons/plus-circle.svg" %} Add Phone
</button>
{% endif %}
</div>
<div class="row g-3 mb-4">
<div class="col-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon stat-icon-total">{% include "icons/collection.svg" %}</div>
<div>
<div class="stat-value">{{ stats.total }}</div>
<div class="stat-label">Total Phones</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon stat-icon-warning">{% include "icons/exclamation-triangle.svg" %}</div>
<div>
<div class="stat-value">{{ stats.expiring_soon }}</div>
<div class="stat-label">Expiring Soon</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon stat-icon-danger">{% include "icons/calendar-x.svg" %}</div>
<div>
<div class="stat-value">{{ stats.expired }}</div>
<div class="stat-label">Expired</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon stat-icon-success">{% include "icons/calendar-check.svg" %}</div>
<div>
<div class="stat-value">{{ stats.eligible_now }}</div>
<div class="stat-label">Eligible Now</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon stat-icon-headoffice">{% include "icons/building.svg" %}</div>
<div>
<div class="stat-value">{{ stats.headoffice }}</div>
<div class="stat-label">QWE Headoffice &middot; {{ stats.headoffice_pct }}%</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon stat-icon-restaurant">{% include "icons/shop.svg" %}</div>
<div>
<div class="stat-value">{{ stats.restaurant }}</div>
<div class="stat-label">KFC Restaurant &middot; {{ stats.restaurant_pct }}%</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon stat-icon-verified">{% include "icons/patch-check.svg" %}</div>
<div>
<div class="stat-value">{{ stats.verified }}</div>
<div class="stat-label">Verified</div>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-card">
<div class="stat-icon stat-icon-unverified">{% include "icons/patch-exclamation.svg" %}</div>
<div>
<div class="stat-value">{{ stats.not_verified }}</div>
<div class="stat-label">Not Verified</div>
</div>
</div>
</div>
</div>
<div class="modern-card mb-0">
<div class="table-toolbar d-flex flex-wrap align-items-center justify-content-between gap-2">
<div class="search-box">
{% include "icons/search.svg" %}
<input type="text" id="phoneSearch" class="form-control" placeholder="Search phones, owners, venues, comments...">
</div>
<div class="text-muted small" id="phoneRowCount"></div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 modern-table" id="phonesTable">
<thead>
<tr>
{% macro sort_link(field, label) -%}
{% set next_dir = 'desc' if sort == field and direction == 'asc' else 'asc' %}
<th>
<a href="{{ url_for('phones_page', sort=field, dir=next_dir) }}" class="sort-link d-inline-flex align-items-center gap-1">
{{ label }}
{% if sort == field %}
<span class="sort-active">{{ '▲' if direction == 'asc' else '▼' }}</span>
{% else %}
<span class="sort-inactive">↕</span>
{% endif %}
</a>
</th>
{%- endmacro %}
{{ sort_link('phone_number', 'Phone Number') }}
{{ sort_link('current_owner', 'Current Owner') }}
{{ sort_link('plan', 'Plan') }}
{{ sort_link('contract_expiration', 'Contract Expiration') }}
{{ sort_link('eligible_for_replacement', 'Eligibility for Replacement') }}
{{ sort_link('venue', 'Venue') }}
{{ sort_link('verified', 'Verified') }}
{{ sort_link('comment', 'Comment') }}
{% if session.role == 'admin' %}<th class="text-end">Actions</th>{% endif %}
</tr>
</thead>
<tbody>
{% for p in phones %}
<tr>
<td class="fw-semibold">{{ p.phone_number }}</td>
<td>{{ p.current_owner or "" }}</td>
<td>{{ p.plan or "" }}</td>
<td>
{% if p.contract_expiration %}
{% if p.contract_expiration < today %}
<span class="badge rounded-pill bg-danger-subtle text-danger-emphasis">Expired</span>
{% elif (p.contract_expiration - today).days <= 60 %}
<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis">Expiring soon</span>
{% endif %}
<span class="d-block text-muted small mt-1">{{ p.contract_expiration.strftime("%d-%m-%Y") }}</span>
{% endif %}
</td>
<td>
{% if not p.eligible_for_replacement or p.eligible_for_replacement <= today %}
<span class="badge rounded-pill bg-success-subtle text-success-emphasis">Eligible now</span>
{% else %}
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis">From {{ p.eligible_for_replacement.strftime("%d-%m-%Y") }}</span>
{% endif %}
</td>
<td>{% if p.venue %}<span class="badge rounded-pill text-bg-light border">{{ p.venue }}</span>{% endif %}</td>
<td>
{% if p.verified == "Yes" %}
<span class="badge rounded-pill bg-success-subtle text-success-emphasis">Yes</span>
{% elif p.verified == "No" %}
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis">No</span>
{% endif %}
</td>
<td class="text-truncate" style="max-width: 220px;" title="{{ p.comment or '' }}">{{ p.comment or "" }}</td>
{% if session.role == 'admin' %}
<td class="text-end">
<div class="d-inline-flex gap-1">
<button type="button" class="btn btn-icon btn-icon-primary" title="Edit"
onclick='openPhoneModal({{ p.to_dict() | tojson }})'>{% include "icons/pencil-square.svg" %}</button>
<button type="button" class="btn btn-icon btn-icon-danger" title="Delete"
onclick="deletePhone({{ p.id }}, '{{ p.phone_number }}')">{% include "icons/trash.svg" %}</button>
</div>
</td>
{% endif %}
</tr>
{% else %}
<tr><td colspan="{{ 9 if session.role == 'admin' else 8 }}" class="text-center text-muted py-5">No phones yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div id="phoneNoResults" class="text-center text-muted py-5" hidden>No phones match your search.</div>
</div>
<!-- Edit / Add modal -->
<div class="modal fade" id="phoneModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form id="phoneForm">
<div class="modal-header">
<h5 class="modal-title" id="phoneModalTitle">Edit Phone</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="phoneId">
<div class="mb-3">
<label class="form-label">Phone Number</label>
<input type="text" class="form-control" id="phoneNumber" required>
<div class="form-text" id="phoneNumberHelp" hidden>Phone number can't be changed after the phone is added.</div>
</div>
<div class="mb-3">
<label class="form-label">Current Owner</label>
<input type="text" class="form-control" id="phoneOwner">
</div>
<div class="mb-3">
<label class="form-label">Plan</label>
<select class="form-select" id="phonePlan">
<option value="">-- Select Plan --</option>
{% for option in plan_options %}
<option value="{{ option }}">{{ option }}</option>
{% endfor %}
</select>
<div class="form-text" id="phonePlanHelp" hidden>Plan can't be changed after the phone is added.</div>
</div>
<div class="mb-3">
<label class="form-label">Contract Expiration</label>
<input type="date" class="form-control" id="phoneContractExpiration">
</div>
<div class="mb-3">
<label class="form-label">Eligibility for Replacement</label>
<input type="date" class="form-control" id="phoneEligible">
<div class="form-text">Leave blank if there is no restriction (eligible now).</div>
</div>
<div class="mb-3">
<label class="form-label">Venue</label>
<select class="form-select" id="phoneVenue">
<option value="">-- Select Venue --</option>
{% for option in venue_options %}
<option value="{{ option }}">{{ option }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Verified</label>
<select class="form-select" id="phoneVerified">
<option value="">-- Select --</option>
{% for option in verified_options %}
<option value="{{ option }}">{{ option }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Comment</label>
<textarea class="form-control" id="phoneComment" rows="3"></textarea>
</div>
<div id="phoneError" class="text-danger small"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const phoneModalEl = document.getElementById('phoneModal');
const phoneModal = new bootstrap.Modal(phoneModalEl);
function openPhoneModal(phone) {
document.getElementById('phoneForm').reset();
document.getElementById('phoneError').textContent = '';
const isEdit = !!phone;
const numberInput = document.getElementById('phoneNumber');
const planSelect = document.getElementById('phonePlan');
numberInput.readOnly = isEdit;
planSelect.disabled = isEdit;
document.getElementById('phoneNumberHelp').hidden = !isEdit;
document.getElementById('phonePlanHelp').hidden = !isEdit;
if (isEdit) {
document.getElementById('phoneModalTitle').textContent = 'Edit Phone';
document.getElementById('phoneId').value = phone.id;
numberInput.value = phone.phone_number || '';
planSelect.value = phone.plan || '';
document.getElementById('phoneContractExpiration').value = phone.contract_expiration || '';
document.getElementById('phoneEligible').value = phone.eligible_for_replacement || '';
document.getElementById('phoneOwner').value = phone.current_owner || '';
document.getElementById('phoneVenue').value = phone.venue || '';
document.getElementById('phoneVerified').value = phone.verified || '';
document.getElementById('phoneComment').value = phone.comment || '';
} else {
document.getElementById('phoneModalTitle').textContent = 'Add Phone';
document.getElementById('phoneId').value = '';
}
phoneModal.show();
}
document.getElementById('phoneForm').addEventListener('submit', async function (e) {
e.preventDefault();
const id = document.getElementById('phoneId').value;
const payload = {
phone_number: document.getElementById('phoneNumber').value,
plan: document.getElementById('phonePlan').value,
contract_expiration: document.getElementById('phoneContractExpiration').value,
eligible_for_replacement: document.getElementById('phoneEligible').value,
current_owner: document.getElementById('phoneOwner').value,
venue: document.getElementById('phoneVenue').value,
verified: document.getElementById('phoneVerified').value,
comment: document.getElementById('phoneComment').value,
};
const url = id ? `/api/phones/${id}` : '/api/phones';
const method = id ? 'PUT' : 'POST';
const resp = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (resp.ok) {
window.location.reload();
} else {
const data = await resp.json().catch(() => ({}));
document.getElementById('phoneError').textContent = data.error || 'Failed to save. Please check the fields.';
}
});
async function deletePhone(id, phoneNumber) {
if (!confirm(`Delete phone ${phoneNumber}? This cannot be undone.`)) return;
const resp = await fetch(`/api/phones/${id}`, { method: 'DELETE' });
if (resp.ok) {
window.location.reload();
} else {
alert('Failed to delete.');
}
}
// Live client-side search across every visible column of the phones table.
(function () {
const searchInput = document.getElementById('phoneSearch');
const table = document.getElementById('phonesTable');
const noResults = document.getElementById('phoneNoResults');
const rowCount = document.getElementById('phoneRowCount');
if (!searchInput || !table) return;
const rows = Array.from(table.querySelectorAll('tbody tr')).filter(tr => tr.children.length > 1);
const totalRows = rows.length;
// Strip accents/diacritics so a search for "n" also matches "ň", "z" matches "ž", etc.
function foldDiacritics(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
const rowText = rows.map(row => foldDiacritics(row.textContent.toLowerCase()));
function applyFilter() {
const q = foldDiacritics(searchInput.value.trim().toLowerCase());
let visible = 0;
rows.forEach((row, i) => {
const match = !q || rowText[i].includes(q);
row.hidden = !match;
if (match) visible++;
});
noResults.hidden = !(q && visible === 0);
table.closest('.table-responsive').hidden = q && visible === 0;
rowCount.textContent = q ? `${visible} of ${totalRows} phones` : `${totalRows} phones`;
}
searchInput.addEventListener('input', applyFilter);
applyFilter();
})();
</script>
{% endblock %}