You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2161 lines
106 KiB
2161 lines
106 KiB
{% extends "base.html" %}
|
|
|
|
{% block title %}
|
|
ONT Details
|
|
{% endblock %}
|
|
|
|
{% block styles %}
|
|
<style>
|
|
/* Simplified modal fixes that work with base.html */
|
|
.modal {
|
|
z-index: 1055 !important;
|
|
}
|
|
|
|
/* QR Code modals need higher z-index */
|
|
#wifiQrModal, #appQrModal, #nokiaCredentialsModal {
|
|
z-index: 1070 !important;
|
|
}
|
|
|
|
#wifiQrModal .modal-dialog,
|
|
#appQrModal .modal-dialog,
|
|
#nokiaCredentialsModal .modal-dialog {
|
|
z-index: 1071 !important;
|
|
}
|
|
|
|
#wifiQrModal .modal-content,
|
|
#appQrModal .modal-content,
|
|
#nokiaCredentialsModal .modal-content {
|
|
z-index: 1072 !important;
|
|
}
|
|
|
|
/* Ensure QR modal backdrops are layered correctly */
|
|
.modal-backdrop.show {
|
|
z-index: 1050 !important;
|
|
}
|
|
|
|
/* When QR modal is open, its backdrop should be higher */
|
|
#wifiQrModal ~ .modal-backdrop,
|
|
#appQrModal ~ .modal-backdrop,
|
|
#nokiaCredentialsModal ~ .modal-backdrop {
|
|
z-index: 1065 !important;
|
|
}
|
|
|
|
/* Fix for modal interaction */
|
|
.modal-dialog {
|
|
position: relative;
|
|
z-index: inherit;
|
|
}
|
|
|
|
.modal-content {
|
|
position: relative;
|
|
z-index: inherit;
|
|
}
|
|
|
|
/* Ensure modals can be interacted with */
|
|
.modal.show {
|
|
display: block !important;
|
|
}
|
|
|
|
.modal.show .modal-dialog {
|
|
pointer-events: all !important;
|
|
}
|
|
|
|
.modal.show .modal-content {
|
|
pointer-events: all !important;
|
|
}
|
|
/* Fix QR code centering in the credentials modal */
|
|
.qr-code-container {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 120px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.qr-code-container img {
|
|
max-width: 100px;
|
|
max-height: 100px;
|
|
width: auto;
|
|
height: auto;
|
|
}
|
|
|
|
/* Ensure the QR code section has proper text alignment */
|
|
#wifiQrContainer,
|
|
#appQrContainer {
|
|
text-align: center;
|
|
}
|
|
|
|
/* Center the "no QR code available" message */
|
|
.qr-code-container p {
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100px;
|
|
}
|
|
</style>
|
|
|
|
|
|
<script>
|
|
function form_submit() {
|
|
document.getElementById("ontswapform").submit();
|
|
}
|
|
|
|
function form_tempservice_submit() {
|
|
document.getElementById("tempserviceform").submit();
|
|
}
|
|
|
|
function form_addBeacon_submit() {
|
|
document.getElementById("addBeaconform").submit();
|
|
}
|
|
|
|
// Copy to clipboard function for credentials
|
|
|
|
// Simplified modal management that works with Bootstrap 5.3.2
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('Modal: DOM loaded, initializing modal functionality');
|
|
|
|
// Copy to clipboard function for credentials - WORKS IN NON-SECURE CONTEXTS
|
|
window.copyToClipboard = function(elementId, buttonElement) {
|
|
console.log('Copy function called for element:', elementId);
|
|
console.log('Secure context:', window.isSecureContext);
|
|
console.log('Clipboard API available:', !!navigator.clipboard);
|
|
|
|
const element = document.getElementById(elementId);
|
|
if (!element) {
|
|
console.error('Copy function: Element not found -', elementId);
|
|
return;
|
|
}
|
|
|
|
const text = element.textContent.trim();
|
|
console.log('Copying text:', text);
|
|
|
|
// Get the button that was clicked
|
|
const button = buttonElement || event.target.closest('button');
|
|
if (!button) {
|
|
console.error('Copy function: Button not found');
|
|
return;
|
|
}
|
|
|
|
// Check if we're in a secure context AND clipboard API is available
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
console.log('Using Clipboard API (secure context)');
|
|
navigator.clipboard.writeText(text)
|
|
.then(() => {
|
|
console.log('Copy successful via Clipboard API');
|
|
showCopySuccess(button);
|
|
})
|
|
.catch(err => {
|
|
console.error('Clipboard API failed:', err);
|
|
fallbackCopy(text, button);
|
|
});
|
|
} else {
|
|
console.log('Using fallback copy method (non-secure context or no Clipboard API)');
|
|
fallbackCopy(text, button);
|
|
}
|
|
};
|
|
|
|
|
|
// Fallback copy method
|
|
function fallbackCopy(text, button) {
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = text;
|
|
textarea.style.position = 'fixed';
|
|
textarea.style.left = '-999999px';
|
|
textarea.style.top = '-999999px';
|
|
document.body.appendChild(textarea);
|
|
|
|
try {
|
|
textarea.focus();
|
|
textarea.select();
|
|
textarea.setSelectionRange(0, 99999); // For mobile devices
|
|
const success = document.execCommand('copy');
|
|
console.log('Fallback copy result:', success);
|
|
|
|
if (success) {
|
|
showCopySuccess(button);
|
|
} else {
|
|
showCopyError(button);
|
|
}
|
|
} catch (err) {
|
|
console.error('Fallback copy failed:', err);
|
|
showCopyError(button);
|
|
} finally {
|
|
document.body.removeChild(textarea);
|
|
}
|
|
}
|
|
|
|
// Show success feedback
|
|
function showCopySuccess(button) {
|
|
const originalContent = button.innerHTML;
|
|
const originalClasses = button.className;
|
|
|
|
button.innerHTML = '<i class="bi bi-check"></i>';
|
|
button.classList.remove('btn-outline-secondary');
|
|
button.classList.add('btn-success');
|
|
|
|
setTimeout(() => {
|
|
button.innerHTML = originalContent;
|
|
button.className = originalClasses;
|
|
}, 1500);
|
|
}
|
|
|
|
// Show error feedback
|
|
function showCopyError(button) {
|
|
const originalContent = button.innerHTML;
|
|
const originalClasses = button.className;
|
|
|
|
button.innerHTML = '<i class="bi bi-x"></i>';
|
|
button.classList.remove('btn-outline-secondary');
|
|
button.classList.add('btn-danger');
|
|
|
|
setTimeout(() => {
|
|
button.innerHTML = originalContent;
|
|
button.className = originalClasses;
|
|
}, 1500);
|
|
}
|
|
|
|
// Handle QR Modal interactions
|
|
// Handle Nokia Credentials Modal
|
|
const nokiaCredentialsModal = document.getElementById('nokiaCredentialsModal');
|
|
|
|
if (nokiaCredentialsModal) {
|
|
console.log('Modal: Nokia Credentials Modal found, setting up event listeners');
|
|
|
|
nokiaCredentialsModal.addEventListener('show.bs.modal', function (event) {
|
|
console.log('Modal: Nokia Credentials Modal show event triggered');
|
|
const button = event.relatedTarget;
|
|
|
|
if (!button) {
|
|
console.error('Modal: No button found in event.relatedTarget');
|
|
return;
|
|
}
|
|
|
|
// Extract data from button attributes
|
|
const username = button.getAttribute('data-username');
|
|
const password = button.getAttribute('data-password');
|
|
const superusername = button.getAttribute('data-superusername');
|
|
const superpassword = button.getAttribute('data-superpassword');
|
|
const ssid = button.getAttribute('data-ssid');
|
|
const wifikey = button.getAttribute('data-wifikey');
|
|
const wifiQrData = button.getAttribute('data-wifiQR');
|
|
const appQrData = button.getAttribute('data-appQR');
|
|
|
|
console.log('Modal: Credential data loaded:', { username, ssid });
|
|
console.log('Modal: WiFi QR Data:', wifiQrData);
|
|
console.log('Modal: App QR Data:', appQrData);
|
|
|
|
// Update modal content
|
|
if (document.getElementById('adminUsername')) {
|
|
document.getElementById('adminUsername').textContent = username || 'admin';
|
|
}
|
|
if (document.getElementById('adminPassword')) {
|
|
document.getElementById('adminPassword').textContent = password || 'password';
|
|
}
|
|
if (document.getElementById('superUsername')) {
|
|
document.getElementById('superUsername').textContent = superusername || 'superadmin';
|
|
}
|
|
if (document.getElementById('superPassword')) {
|
|
document.getElementById('superPassword').textContent = superpassword || 'password';
|
|
}
|
|
if (document.getElementById('wifiSSID')) {
|
|
document.getElementById('wifiSSID').textContent = ssid || 'ssid';
|
|
}
|
|
if (document.getElementById('wifiPassword')) {
|
|
document.getElementById('wifiPassword').textContent = wifikey || 'password';
|
|
}
|
|
|
|
// Handle QR codes
|
|
const wifiQrPreview = document.getElementById('wifiQrPreview');
|
|
const wifiQrFull = document.getElementById('wifiQrFull');
|
|
const appQrPreview = document.getElementById('appQrPreview');
|
|
const appQrFull = document.getElementById('appQrFull');
|
|
const wifiQrContainer = document.getElementById('wifiQrContainer');
|
|
const appQrContainer = document.getElementById('appQrContainer');
|
|
|
|
// Handle WiFi QR Code
|
|
if (wifiQrData && wifiQrData !== 'None' && wifiQrData !== '' && wifiQrData !== 'null') {
|
|
const wifiSrc = 'data:image/png;base64,' + wifiQrData;
|
|
if (wifiQrPreview) {
|
|
wifiQrPreview.src = wifiSrc;
|
|
wifiQrPreview.style.display = 'block';
|
|
}
|
|
if (wifiQrFull) {
|
|
wifiQrFull.src = wifiSrc;
|
|
}
|
|
if (wifiQrContainer) {
|
|
wifiQrContainer.style.pointerEvents = 'auto';
|
|
wifiQrContainer.style.cursor = 'pointer';
|
|
}
|
|
console.log('Modal: WiFi QR code loaded successfully');
|
|
} else {
|
|
if (wifiQrPreview) {
|
|
wifiQrPreview.style.display = 'none';
|
|
}
|
|
if (wifiQrContainer) {
|
|
wifiQrContainer.innerHTML = '<p class="text-muted small mb-0">No WiFi QR code available</p>';
|
|
wifiQrContainer.style.pointerEvents = 'none';
|
|
wifiQrContainer.style.cursor = 'default';
|
|
}
|
|
console.log('Modal: No WiFi QR code data available');
|
|
}
|
|
|
|
// Handle App QR Code
|
|
if (appQrData && appQrData !== 'None' && appQrData !== '' && appQrData !== 'null') {
|
|
const appSrc = 'data:image/png;base64,' + appQrData;
|
|
if (appQrPreview) {
|
|
appQrPreview.src = appSrc;
|
|
appQrPreview.style.display = 'block';
|
|
}
|
|
if (appQrFull) {
|
|
appQrFull.src = appSrc;
|
|
}
|
|
if (appQrContainer) {
|
|
appQrContainer.style.pointerEvents = 'auto';
|
|
appQrContainer.style.cursor = 'pointer';
|
|
}
|
|
console.log('Modal: App QR code loaded successfully');
|
|
} else {
|
|
if (appQrPreview) {
|
|
appQrPreview.style.display = 'none';
|
|
}
|
|
if (appQrContainer) {
|
|
appQrContainer.innerHTML = '<p class="text-muted small mb-0">No App QR code available</p>';
|
|
appQrContainer.style.pointerEvents = 'none';
|
|
appQrContainer.style.cursor = 'default';
|
|
}
|
|
console.log('Modal: No App QR code data available');
|
|
}
|
|
});
|
|
|
|
nokiaCredentialsModal.addEventListener('shown.bs.modal', function() {
|
|
console.log('Modal: Nokia Credentials Modal is now visible and interactive');
|
|
});
|
|
|
|
nokiaCredentialsModal.addEventListener('hidden.bs.modal', function() {
|
|
console.log('Modal: Nokia Credentials Modal is now hidden');
|
|
});
|
|
} else {
|
|
console.error('Modal: Nokia Credentials Modal element not found!');
|
|
}
|
|
|
|
// Handle QR Modal interactions
|
|
const qrModals = ['wifiQrModal', 'appQrModal'];
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% set rebooting = false %}
|
|
|
|
<div class="container py-4">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<!-- Page Header -->
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-header bg-secondary-subtle border-secondary-subtle text-dark">
|
|
<div class="row align-items-center">
|
|
<div class="col">
|
|
<h3 class="mb-0">{{ locID.LocationIdentifier }}</h3>
|
|
<strong class="text-end">Unit:</strong>
|
|
|
|
<span class="badge bg-light text-dark border fs-6">{{ locID.UnitIdentifier }}</span>
|
|
|
|
|
|
</div>
|
|
<div class="col-auto text-end">
|
|
<a href="{{ url_for('location.location_parent_detail', parentID=parent_loc.id) }}"
|
|
class="text-decoration-none text-dark">
|
|
<h5 class="mb-0 opacity-75">{{ parent_loc.Alias }}</h5>
|
|
<small class="text-end d-block">
|
|
{{ locID.StreetAddress }}<br>
|
|
{{ locID.Suburb }} {{ locID.State }} {{ locID.Postcode }}
|
|
</small>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Flash Messages -->
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
{% if category == "ontReboot" %}
|
|
{% set rebooting = true %}
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% elif category == "ontSwap" %}
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% elif category == "success" %}
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% elif category == "BeaconAdd" %}
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<div class="row">
|
|
<!-- Left Column - ONT Details -->
|
|
<div class="col-lg-8">
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-header bg-light">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-router me-2"></i>ONT Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
|
|
<div class="col-md-6 mb-2">
|
|
<strong>ONT Name:</strong><br>
|
|
{% if adtran_details['Service']['downlink-endpoint'] is defined %}
|
|
<span class="text-muted">{{ adtran_details['Service']['downlink-endpoint']['interface-endpoint']['device-name'] }}</span>
|
|
{% else %}
|
|
<span class="text-muted">none</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="col-md-6 mb-2">
|
|
<strong>ONT Model:</strong><br>
|
|
<span class="text-muted">{{ adtran_details['ONTModel'] }}</span>
|
|
</div>
|
|
<div class="col-6 mb-2">
|
|
<strong>Serial Number:</strong><br>
|
|
{% if adtran_details['SerialNumber'] == adtran_details['SerialNumber_db'] %}
|
|
<span class="text-muted font-monospace">{{ adtran_details['SerialNumber'] }}</span>
|
|
{% else %}
|
|
<div class="alert alert-danger py-2 mb-0">
|
|
<strong>Mismatch Detected!</strong><br>
|
|
OLT: <span class="font-monospace">{{ adtran_details['SerialNumber'] }}</span><br>
|
|
Hades: <span class="font-monospace">{{ adtran_details['SerialNumber_db'] }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="col-md-6 mb-2">
|
|
<strong>MAC Address:</strong><br>
|
|
<span class="text-muted font-monospace">{{ adtran_details['MACAddress_db'] }}</span>
|
|
</div>
|
|
<div class="col-md-6 mb-2">
|
|
<strong>OLT:</strong><br>
|
|
<span class="text-muted font-monospace">{{ adtran_details['OLT'] }}</span>
|
|
</div>
|
|
<div class="col-md-6 mb-2">
|
|
<strong>OLT PON Interface:</strong><br>
|
|
<span class="text-muted font-monospace">{{ adtran_details['OLT_PON_Interface'] }}</span>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ethernet Port Status -->
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-header bg-light">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-ethernet me-2"></i>Ethernet Port Status
|
|
</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if adtran_details['intfStatus'] and adtran_details['intfStatus']|length > 0 %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th scope="col">Port</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">Speed</th>
|
|
<th scope="col">Duplex</th>
|
|
<th scope="col">Auto-Negotiated</th>
|
|
<th scope="col">Admin Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for port_key, port_data in adtran_details['intfStatus']|dictsort %}
|
|
{% if port_data['port-number'] != "0" %}
|
|
<tr class="align-middle">
|
|
<td>
|
|
<span class="badge bg-secondary px-3 py-2">Port {{ port_data['port-number'] }}</span>
|
|
</td>
|
|
<td>
|
|
{% if port_data['status'] == "up" %}
|
|
<span class="badge bg-success px-3 py-2">UP</span>
|
|
{% else %}
|
|
<span class="badge bg-danger px-3 py-2">DOWN</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if port_data['actual-speed'] %}
|
|
{% set speed_mbps = (port_data['actual-speed'] * 1000)|int %}
|
|
{% if speed_mbps >= 1000 %}
|
|
<span class="text-dark">{{ (speed_mbps / 1000)|int }}Gbps</span>
|
|
{% else %}
|
|
<span class="text-dark">{{ speed_mbps }}Mbps</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if port_data['actual-duplex'] == "full" %}
|
|
<span class="badge bg-success px-2 py-1">Full</span>
|
|
{% elif port_data['actual-duplex'] == "half" %}
|
|
<span class="badge bg-warning text-dark px-2 py-1">Half</span>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if port_data['auto-negotiated'] %}
|
|
<span class="badge bg-info px-2 py-1">Yes</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary px-2 py-1">No</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if port_data['admin-status'] == "up" %}
|
|
<span class="badge bg-success px-2 py-1">Enabled</span>
|
|
{% else %}
|
|
<span class="badge bg-danger px-2 py-1">Disabled</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-ethernet text-muted" style="font-size: 3rem;"></i>
|
|
<h5 class="text-muted mt-3">No Port Information</h5>
|
|
<p class="text-muted">No ethernet port data available for this ONT.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Services Section -->
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-globe2 me-2"></i>Services
|
|
</h5>
|
|
<a href="#newTempServiceModal" class="btn btn-sm btn-primary" data-bs-toggle="modal">
|
|
New Temp Service
|
|
</a>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if adtran_details['srvList']|length > 0 %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Service ID</th>
|
|
<th>Profile</th>
|
|
<th>VLANs</th>
|
|
<th>Customer</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for data in adtran_details['srvList'] %}
|
|
<tr class="align-middle">
|
|
{% if data.hades_service != "none" %}
|
|
<td>
|
|
{% if data['customer_id'] == "none" %}
|
|
<a href="{{ url_for('services.service_id', service_ID=data.hades_service.id) }}"
|
|
class="text-decoration-none">
|
|
{{ data['service-id'] }}
|
|
</a>
|
|
{% else %}
|
|
<a href="{{ url_for('services.service_id_splynx', service_ID=data.hades_service.id) }}"
|
|
class="text-decoration-none">
|
|
{{ data['service-id'] }}
|
|
</a>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ data['profile'] }}</td>
|
|
<td><span class="badge bg-secondary">{{ data['uplink-vlans'] }}</span></td>
|
|
|
|
{% if data['customer_id'] == "none" %}
|
|
<td><div class="text-secondary text-opacity-50">No Splynx Service</div></td>
|
|
{% else %}
|
|
<td>
|
|
<a href="https://billing.interphone.com.au/admin/customers/view?id={{ data['customer_id'] }}"
|
|
target="_blank" rel="noopener noreferrer" class="text-decoration-none">
|
|
{{ data['customer_name'] }}
|
|
</a>
|
|
</td>
|
|
{% endif %}
|
|
|
|
<td>
|
|
{% if data['status'] == "Online" %}
|
|
<span class="badge bg-success">Online</span>
|
|
{% elif data['status'] == "stopped" %}
|
|
<span class="badge bg-warning text-dark">Paused</span>
|
|
{% elif data['status'] == "active" %}
|
|
<span class="badge bg-danger">Offline</span>
|
|
{% elif data['status'] == "no service found" %}
|
|
<span class=""></span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ data['status'] }}</span>
|
|
{% endif %}
|
|
</td>
|
|
{% else %}
|
|
<td colspan="5"><em class="text-muted">No services configured</em></td>
|
|
{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4">
|
|
<p class="text-muted mb-0">No services configured</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nokia Beacon -->
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-wifi me-2"></i>Nokia Beacon
|
|
</h5>
|
|
<a href="#addBeaconModal" class="btn btn-sm btn-primary" data-bs-toggle="modal">
|
|
Add Beacon
|
|
</a>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if nokias|length > 0 %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Model</th>
|
|
<th>Serial</th>
|
|
<th>MAC</th>
|
|
<th>Corteca</th>
|
|
<th>Status</th>
|
|
<th>Admin</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for data in nokias %}
|
|
<tr class="align-middle">
|
|
{% if data %}
|
|
<td>
|
|
{{ data['Model'] }}
|
|
</td>
|
|
<td>{{ data['Serial'] }}</td>
|
|
{% set nokia_mac = data['MAC'][:2] ~ '-' ~ data['MAC'][2:4] ~ '-' ~ data['MAC'][4:6] ~ '-' ~ data['MAC'][6:8] ~ '-' ~ data['MAC'][8:10] ~ '-' ~ data['MAC'][10:12] %}
|
|
<td>{{ nokia_mac }}</td>
|
|
<td>
|
|
|
|
<a href="https://homecontroller.apeu.wifi.nokia.com/interphone/home-troubleshooting/dashboard?mac={{ nokia_mac }}"
|
|
target="_blank" rel="noopener noreferrer" class="text-decoration-none">
|
|
View
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<!-- <span class="badge bg-secondary">Not Yet Implemented</span> -->
|
|
{% if nokia_status[data['Serial']] %}
|
|
<span class="badge bg-success">Online</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Offline</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" style="--bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#nokiaCredentialsModal"
|
|
data-username="{{ data['Username'] }}"
|
|
data-password="{{ data['Password'] }}"
|
|
data-superusername="{{ data['SuperUsername'] }}"
|
|
data-superpassword="{{ data['SuperPassword'] }}"
|
|
data-ssid="{{ data['SSID'] }}"
|
|
data-wifikey="{{ data['WiFiKey'] }}"
|
|
data-wifiQR="{{ beacon_qr[data['id']]['wifiQR'] }}"
|
|
data-appQR="{{ beacon_qr[data['id']]['nokiaQR'] }}">
|
|
<i class="bi bi-key"></i> Credentials
|
|
</button>
|
|
</td>
|
|
{% else %}
|
|
<td colspan="6"><em class="text-muted">No Beacons assigned</em></td>
|
|
{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4">
|
|
<p class="text-muted mb-0">No Beacons assigned</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<!-- Events Section -->
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-light">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-clock-history me-2"></i>Recent Events
|
|
</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if adtran_details['eventList']|length > 0 %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Timestamp</th>
|
|
<th>Event</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for data in adtran_details['eventList'] %}
|
|
<tr class="align-middle">
|
|
<td class="text-nowrap">{{ data['timestamp'] }}</td>
|
|
<td>{{ data['event'] }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4">
|
|
<p class="text-muted mb-0">No recent events</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column - Status & Actions -->
|
|
<div class="col-lg-4">
|
|
<!-- Pending Job Alert -->
|
|
{% if pending_swap_job %}
|
|
<div class="card mb-4 shadow-sm border-warning">
|
|
<div class="card-header bg-warning text-dark">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>Pending ONT Swap Job
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-2">An ONT swap job is currently in progress for this location.</p>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<small class="text-muted">
|
|
Started: {{ pending_swap_job.Created.strftime('%Y-%m-%d %H:%M:%S') }}
|
|
</small>
|
|
<a href="{{ url_for('location.ont_swap_progress', locid=locID['id'], job_id=pending_swap_job.id) }}"
|
|
class="btn btn-sm btn-warning">
|
|
<i class="bi bi-eye me-2"></i>View Progress
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Status Overview -->
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-header bg-light">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-activity me-2"></i>Status Overview
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Service Class -->
|
|
<div class="d-flex align-items-center mb-2">
|
|
<strong class="flex-shrink-0" style="width: 140px;">Service Class:</strong>
|
|
<div>
|
|
{% if adtran_details['ServiceClass'][0] %}
|
|
<span class="badge bg-success px-3 py-2 me-1">{{ locID.ServiceClass }}</span>
|
|
<span class="badge bg-success px-3 py-2">{{ adtran_details['ServiceClass'][1] }}</span>
|
|
{% else %}
|
|
<span class="badge bg-danger px-3 py-2 me-1">{{ locID.ServiceClass }}</span>
|
|
<span class="badge bg-danger px-3 py-2">{{ adtran_details['ServiceClass'][1] }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Connection Status -->
|
|
<div class="d-flex align-items-center mb-2">
|
|
<strong class="flex-shrink-0" style="width: 140px;">Connected:</strong>
|
|
{% if adtran_details['Connected'] and rebooting == false %}
|
|
<span class="badge bg-success px-3 py-2">{{ adtran_details['Connected'] }}</span>
|
|
{% else %}
|
|
<span class="badge bg-danger px-3 py-2">{{ adtran_details['Connected'] }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- State -->
|
|
<div class="d-flex align-items-center mb-2">
|
|
<strong class="flex-shrink-0" style="width: 140px;">State:</strong>
|
|
{% if adtran_details['State'] == "Deployed" %}
|
|
<span class="badge bg-success px-3 py-2">{{ adtran_details['State'] }}</span>
|
|
{% else %}
|
|
<span class="badge bg-danger px-3 py-2">{{ adtran_details['State'] }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Uptime -->
|
|
<div class="d-flex align-items-center mb-2">
|
|
{% if adtran_details['Details'] is defined and rebooting == false %}
|
|
{% set onu_uptime = adtran_details['Details']['single-slot-device']['onu']['uptime'] %}
|
|
{% else %}
|
|
{% set onu_uptime = 0 %}
|
|
{% endif %}
|
|
<strong class="flex-shrink-0" style="width: 140px;">ONU Uptime:</strong>
|
|
{% if onu_uptime == 0 %}
|
|
<span class="badge bg-danger px-3 py-2">Not Up</span>
|
|
{% elif onu_uptime > 259200 %}
|
|
<span class="badge bg-success px-3 py-2">{{ adtran_details['ONU Uptime'] }}</span>
|
|
{% elif onu_uptime > 86400 and onu_uptime < 259200 %}
|
|
<span class="badge bg-warning text-dark px-3 py-2">{{ adtran_details['ONU Uptime'] }}</span>
|
|
{% else %}
|
|
<span class="badge bg-danger px-3 py-2">{{ adtran_details['ONU Uptime'] }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Receive Light -->
|
|
<div class="d-flex align-items-center mb-2">
|
|
<strong class="flex-shrink-0" style="width: 140px;">Receive Light:</strong>
|
|
{% if adtran_details['Cached_RX'] is defined and rebooting == false %}
|
|
{% set rx_power = adtran_details['Cached_RX'] %}
|
|
{% if rx_power == 0 %}
|
|
<span class="badge bg-danger px-3 py-2">0 dBm</span>
|
|
{% elif rx_power > parent_loc.MaxONT_dB %}
|
|
<span class="badge bg-success px-3 py-2">{{ adtran_details['Received Light'] }}</span>
|
|
{% elif rx_power < parent_loc.MaxONT_dB and rx_power > parent_loc.MaxONT_dB-20 %}
|
|
<span class="badge bg-warning text-dark px-3 py-2">{{ adtran_details['Received Light'] }}</span>
|
|
{% else %}
|
|
<span class="badge bg-danger px-3 py-2">{{ adtran_details['Received Light'] }}</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="badge bg-secondary px-3 py-2">No Data</span>
|
|
{% endif %}
|
|
</div>
|
|
<!-- Commission Light -->
|
|
<div class="d-flex align-items-center mb-2">
|
|
<strong class="flex-shrink-0" style="width: 140px;">Commissioned:</strong>
|
|
{% if ont.Commissioned_RX != None %}
|
|
<span class="badge bg-secondary px-3 py-2 me-2">{{ ont.Commissioned_RX/10 }} dBm</span>
|
|
<span class="badge bg-secondary px-3 py-2">{{ ont.Commissioned_Date.strftime('%d-%m-%Y') }}</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary px-3 py-2">Not Commissioned</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Fibre Distance -->
|
|
<div class="d-flex align-items-center mb-2">
|
|
<strong class="flex-shrink-0" style="width: 140px;">Fibre Distance:</strong>
|
|
{% if adtran_details['Distance'] is defined and adtran_details['Distance'] != None %}
|
|
<span class="badge text-bg-secondary px-3 py-2">{{ adtran_details['Distance'] }}m</span>
|
|
{% else %}
|
|
<span class="badge text-bg-secondary px-3 py-2">{{ adtran_details['Distance'] }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Port Status -->
|
|
{% for key, value in adtran_details['intfStatus']|dictsort %}
|
|
{% if value['port-number'] != "0" %}
|
|
<div class="d-flex align-items-center mb-2">
|
|
<strong class="flex-shrink-0" style="width: 140px;">Port {{ value['port-number'] }}:</strong>
|
|
{% if value['status'] == "up" %}
|
|
<span class="badge bg-success px-3 py-2">{{ value['status'].upper() }}</span>
|
|
{% else %}
|
|
<span class="badge bg-danger text-dark px-3 py-2">{{ value['status'].upper() }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-light">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-tools me-2"></i>Actions
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
{% if current_user.Permissions in ["Admin", "Project Delivery", "Helpdesk L1", "Helpdesk L2"] %}
|
|
<!-- Permission-specific actions can be added here -->
|
|
<a href="#swapModal" class="btn btn-outline-primary" data-bs-toggle="modal">
|
|
<i class="bi bi-arrow-repeat"></i> Swap ONT
|
|
</a>
|
|
|
|
<a href="#complexSwapModal" class="btn btn-outline-info" data-bs-toggle="modal">
|
|
<i class="bi bi-gear"></i> Complex Swap
|
|
</a>
|
|
|
|
{% endif %}
|
|
<a href="#rebootModal" class="btn btn-outline-warning" data-bs-toggle="modal" style="color: #b45309; border-color: #b45309;">
|
|
<i class="bi bi-power"></i> Reboot ONT
|
|
</a>
|
|
{% if current_user.Permissions in ["Admin", "Finance", "Project Delivery"] %}
|
|
<hr>
|
|
{% if locID.ServiceClass in [12,13] and current_user.Permissions in ["Admin", "Finance", "Helpdesk L2"] %}
|
|
<a href="#provisionServiceModal" class="btn btn-outline-success" data-bs-toggle="modal">
|
|
<i class="bi bi-plus-circle"></i> Provision Service
|
|
</a>
|
|
{% elif not adtran_details['Connected'] and adtran_details['State'] == "none" and current_user.Permissions in ["Admin", "Project Delivery", "Helpdesk L2"] %}
|
|
<a href="{{ url_for('onts.ont_provision', locid=locID['id']) }}" class="btn btn-outline-success">
|
|
<i class="bi bi-router"></i> Provision ONT
|
|
</a>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reboot ONT Modal -->
|
|
<div class="modal fade" id="rebootModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header text-dark">
|
|
<h5 class="modal-title">Reboot ONT Confirmation</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-dark">Are you sure you want to reboot <strong>{{ ont['ONTIdentifier'] }}</strong>?</p>
|
|
<div class="alert alert-warning">
|
|
<small><strong>Warning:</strong> This action will affect customer service.</small>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<a href="{{ url_for('location.location_id_reboot', locid=locID['id']) }}" class="btn btn-warning">
|
|
Confirm Reboot
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Swap ONT Modal -->
|
|
<div class="modal fade" id="swapModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header text-dark">
|
|
<h5 class="modal-title">Swap ONT</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form method="POST" action="/location/ID/{{ ont['Location_ID'] }}/swap" id="ontswapform">
|
|
<input type="hidden" name="ont_id" value="{{ ont['Location_ID'] }}">
|
|
<div class="mb-2">
|
|
<label for="new_serial" class="form-label text-dark">New Serial Number</label>
|
|
<input type="text" class="form-control font-monospace" id="new_serial" name="new_serial"
|
|
placeholder="ADTN11112222" required>
|
|
</div>
|
|
<div class="alert alert-info">
|
|
<small><strong>Note:</strong> Swap can only work with the same ONT model.</small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="form_submit()">Swap ONT</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Provision Temp Internet Service Modal -->
|
|
<div class="modal fade" id="newTempServiceModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title text-dark">
|
|
<i class="bi bi-plus-circle me-2"></i>New Temp Internet Service
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Step 1: Enter Splynx ID -->
|
|
<div id="tempStep1" class="temp-provision-step">
|
|
<div class="mb-3">
|
|
<h6 class="text-dark mb-3">
|
|
<i class="bi bi-search me-2"></i>Customer Lookup
|
|
</h6>
|
|
<label for="temp_lookup_splynx_id" class="form-label text-dark">Splynx Customer ID</label>
|
|
<input type="number" class="form-control" id="temp_lookup_splynx_id" placeholder="Enter customer ID" required>
|
|
<div class="form-text text-muted">Enter the Splynx customer ID to fetch customer details</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="tempLoading" class="text-center py-4 d-none">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 text-muted mb-0">Fetching customer details...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="tempCustomerError" class="alert alert-danger d-none">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
<span id="tempErrorMessage">Customer not found or error occurred</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Confirm Customer & Configure Service -->
|
|
<div id="tempStep2" class="temp-provision-step d-none">
|
|
<div class="mb-4">
|
|
<h6 class="text-dark mb-3">
|
|
<i class="bi bi-person-check me-2"></i>Confirm Customer Details
|
|
</h6>
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<div id="tempCustomerDetails">
|
|
<!-- Customer details will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="tempServiceForm">
|
|
<input type="hidden" id="temp_confirmed_splynx_id" name="splynx_id">
|
|
<input type="hidden" name="ont_id" value="{{ ont['Location_ID'] }}">
|
|
|
|
<div class="mb-3">
|
|
<label for="temp_uni_interface" class="form-label text-dark">UNI Interface</label>
|
|
<select class="form-select" id="temp_uni_interface" name="uni_interface" required>
|
|
<option value="">Select UNI interface...</option>
|
|
{% for uni in ont_uni %}
|
|
<option value="{{ uni.id }}">{{ uni.UNI_Name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="alert alert-info">
|
|
<small>
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
<strong>Note:</strong> This will create a temporary internet service for the customer.
|
|
</small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
|
|
<!-- Step 1 buttons -->
|
|
<div id="tempStep1Buttons">
|
|
<button type="button" class="btn btn-primary px-4" id="tempNextBtn" onclick="fetchTempCustomerDetails()">
|
|
<i class="bi bi-arrow-right me-2"></i>Next
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Step 2 buttons -->
|
|
<div id="tempStep2Buttons" class="d-none">
|
|
<button type="button" class="btn btn-outline-secondary px-4" id="tempBackBtn" onclick="goBackToTempStep1()">
|
|
<i class="bi bi-arrow-left me-2"></i>Back
|
|
</button>
|
|
<button type="button" class="btn btn-primary px-4" id="tempProvisionBtn" onclick="provisionTempService()">
|
|
<i class="bi bi-plus-circle me-2"></i>Create Temp Service
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Temp Service Success Modal -->
|
|
<div class="modal fade" id="tempServiceSuccessModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-check-circle me-2"></i>Temp Service Created Successfully
|
|
</h5>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-center py-3">
|
|
<i class="bi bi-check-circle-fill text-success" style="font-size: 3rem;"></i>
|
|
<h6 class="mt-3 mb-3 text-dark">Success!</h6>
|
|
<div id="tempSuccessMessage" class="text-dark">
|
|
Temporary internet service has been created successfully.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer justify-content-center">
|
|
<button type="button" class="btn btn-primary px-4" onclick="closeTempSuccessModal()">
|
|
<i class="bi bi-x-lg me-2"></i>Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Beacon Modal -->
|
|
<div class="modal fade" id="addBeaconModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header text-dark">
|
|
<h5 class="modal-title">Add Beacon</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form method="POST" action="/location/ID/{{ ont['Location_ID'] }}/addBeacon" id="addBeaconform">
|
|
<input type="hidden" name="ont_id" value="{{ ont['Location_ID'] }}">
|
|
<div class="mb-2">
|
|
<label for="beacon_serial" class="form-label text-dark">Beacon Serial Number</label>
|
|
<input type="text" class="form-control" id="beacon_serial" name="beacon_serial" placeholder="ALCLB459301F" required>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="form_addBeacon_submit()">Add</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Nokia Credentials Modal -->
|
|
<div class="modal fade" id="nokiaCredentialsModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title text-dark">
|
|
<i class="bi bi-router me-2"></i>Nokia Beacon Credentials
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<!-- Admin Credentials -->
|
|
<div class="col-12">
|
|
<div class="card bg-light">
|
|
<div class="card-header bg-primary text-white">
|
|
<h6 class="mb-0">
|
|
<i class="bi bi-person-gear me-2"></i>Administrator Access
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-4">
|
|
<strong>Username:</strong>
|
|
</div>
|
|
<div class="col-8">
|
|
<span class="font-monospace text-dark" id="adminUsername">admin</span>
|
|
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('adminUsername', this)" title="Copy username">
|
|
<i class="bi bi-clipboard"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-4">
|
|
<strong>Password:</strong>
|
|
</div>
|
|
<div class="col-8">
|
|
<span class="font-monospace text-dark" id="adminPassword">password</span>
|
|
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('adminPassword', this)" title="Copy password">
|
|
<i class="bi bi-clipboard"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Super Admin Credentials -->
|
|
<div class="col-12">
|
|
<div class="card bg-light">
|
|
<div class="card-header bg-warning text-dark">
|
|
<h6 class="mb-0">
|
|
<i class="bi bi-shield-exclamation me-2"></i>Super Administrator Access
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-4">
|
|
<strong>Username:</strong>
|
|
</div>
|
|
<div class="col-8">
|
|
<span class="font-monospace text-dark" id="superUsername">superadmin</span>
|
|
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('superUsername', this)" title="Copy username">
|
|
<i class="bi bi-clipboard"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-4">
|
|
<strong>Password:</strong>
|
|
</div>
|
|
<div class="col-8">
|
|
<span class="font-monospace text-dark" id="superPassword">password</span>
|
|
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('superPassword', this)" title="Copy password">
|
|
<i class="bi bi-clipboard"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SSID Credentials -->
|
|
<div class="col-12">
|
|
<div class="card bg-light">
|
|
<div class="card-header bg-info text-white">
|
|
<h6 class="mb-0">
|
|
<i class="bi bi-wifi me-2"></i>WiFi Network Access
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-4">
|
|
<strong>SSID:</strong>
|
|
</div>
|
|
<div class="col-8">
|
|
<span class="font-monospace text-dark" id="wifiSSID">ssid</span>
|
|
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('wifiSSID', this)" title="Copy SSID">
|
|
<i class="bi bi-clipboard"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-4">
|
|
<strong>Password:</strong>
|
|
</div>
|
|
<div class="col-8">
|
|
<span class="font-monospace text-dark" id="wifiPassword">password</span>
|
|
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('wifiPassword', this)" title="Copy password">
|
|
<i class="bi bi-clipboard"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- QR Codes Section -->
|
|
<div class="col-12 mt-3">
|
|
<div class="card bg-light">
|
|
<div class="card-header bg-success text-white">
|
|
<h6 class="mb-0">
|
|
<i class="bi bi-qr-code me-2"></i>QR Codes
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row text-center">
|
|
<div class="col-6">
|
|
<h6 class="text-dark">WiFi Setup</h6>
|
|
<div class="qr-code-container mb-2" style="cursor: pointer;"
|
|
data-bs-toggle="modal" data-bs-target="#wifiQrModal" id="wifiQrContainer">
|
|
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
|
alt="WiFi QR Code" class="img-fluid border rounded"
|
|
style="max-width: 100px; max-height: 100px; display: none;" id="wifiQrPreview">
|
|
</div>
|
|
<small class="text-muted">Click to view full size</small>
|
|
</div>
|
|
<div class="col-6">
|
|
<h6 class="text-dark">Nokia App</h6>
|
|
<div class="qr-code-container mb-2" style="cursor: pointer;"
|
|
data-bs-toggle="modal" data-bs-target="#appQrModal" id="appQrContainer">
|
|
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
|
alt="App QR Code" class="img-fluid border rounded"
|
|
style="max-width: 100px; max-height: 100px; display: none;" id="appQrPreview">
|
|
</div>
|
|
<small class="text-muted">Click to view full size</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-info mt-3">
|
|
<small>
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
<strong>Note:</strong> These are the default credentials for Nokia Beacon devices.
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- WiFi QR Modal -->
|
|
<div class="modal fade" id="wifiQrModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header text-dark">
|
|
<h5 class="modal-title">Nokia WiFi QR Code</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
|
alt="WiFi QR Code" class="img-fluid" id="wifiQrFull">
|
|
<p class="mt-3 text-muted small">Scan with device to connect to WiFi network</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- App QR Modal -->
|
|
<div class="modal fade" id="appQrModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header text-dark">
|
|
<h5 class="modal-title">Nokia App QR Code</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
|
alt="App QR Code" class="img-fluid" id="appQrFull">
|
|
<p class="mt-3 text-muted small">Scan with device to access Nokia app</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Provision Service Modal -->
|
|
<div class="modal fade" id="provisionServiceModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title text-dark">
|
|
<i class="bi bi-plus-circle me-2"></i>Provision Service
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Step 1: Enter Splynx ID -->
|
|
<div id="step1" class="provision-step">
|
|
<div class="mb-3">
|
|
<h6 class="text-dark mb-3">
|
|
<i class="bi bi-search me-2"></i>Customer Lookup
|
|
</h6>
|
|
<label for="lookup_splynx_id" class="form-label text-dark">Splynx Customer ID</label>
|
|
<input type="number" class="form-control" id="lookup_splynx_id" placeholder="Enter customer ID" required>
|
|
<div class="form-text text-muted">Enter the Splynx customer ID to fetch customer details</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="loading" class="text-center py-4 d-none">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 text-muted mb-0">Fetching customer details...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="customerError" class="alert alert-danger d-none">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
<span id="errorMessage">Customer not found or error occurred</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Confirm Customer & Configure Service -->
|
|
<div id="step2" class="provision-step d-none">
|
|
<div class="mb-4">
|
|
<h6 class="text-dark mb-3">
|
|
<i class="bi bi-person-check me-2"></i>Confirm Customer Details
|
|
</h6>
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<div id="customerDetails">
|
|
<!-- Customer details will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="provisionForm">
|
|
<input type="hidden" id="confirmed_splynx_id" name="splynx_id">
|
|
<input type="hidden" name="ont_id" value="{{ ont['Location_ID'] }}">
|
|
|
|
<div class="mb-3">
|
|
<label for="plan_id" class="form-label text-dark">Service Plan</label>
|
|
<select class="form-select" id="plan_id" name="plan_id" required>
|
|
<option value="">Select a service plan...</option>
|
|
{% for plan in plans %}
|
|
<option value="{{ plan.id }}">{{ plan.DisplayName }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="uni_interface" class="form-label text-dark">UNI Interface</label>
|
|
<select class="form-select" id="uni_interface" name="uni_interface" required>
|
|
<option value="">Select UNI interface...</option>
|
|
{% for uni in ont_uni %}
|
|
<option value="{{ uni.id }}">{{ uni.UNI_Name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="vlan_option" class="form-label text-dark">VLAN Configuration</label>
|
|
<div class="mb-2">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="vlan_option" id="vlan_untagged" value="untagged" checked>
|
|
<label class="form-check-label text-dark" for="vlan_untagged">
|
|
Untagged
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="vlan_option" id="vlan_tagged" value="tagged">
|
|
<label class="form-check-label text-dark" for="vlan_tagged">
|
|
Tagged VLAN
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div id="vlan_id_container" style="display: none;">
|
|
<label for="vlan_id" class="form-label text-dark">VLAN ID</label>
|
|
<input type="number" class="form-control" id="vlan_id" name="vlan_id" min="2" max="4096" placeholder="Enter VLAN ID (2-4096)">
|
|
<div class="form-text text-muted">Valid range: 2-4096</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Additional Options -->
|
|
<div class="mb-3">
|
|
<label class="form-label text-dark">Additional Options</label>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="free_trial" name="free_trial">
|
|
<label class="form-check-label text-dark" for="free_trial">
|
|
<strong>Fibre 1000 Free Trial first</strong>
|
|
<div class="form-text text-muted">Enable free trial period before regular service</div>
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="add_development_fee" name="add_development_fee">
|
|
<label class="form-check-label text-dark" for="add_development_fee">
|
|
<strong>Add New Development Fee</strong>
|
|
<div class="form-text text-muted">Add a new development fee to the customer account</div>
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="billing_next_month" name="billing_next_month">
|
|
<label class="form-check-label text-dark" for="billing_next_month">
|
|
<strong>Billing Next Month</strong>
|
|
<div class="form-text text-muted">Set billing to begin from next month</div>
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="add_month_credit" name="add_month_credit" disabled>
|
|
<label class="form-check-label text-dark" for="add_month_credit">
|
|
<strong>Add One Month Credit</strong>
|
|
<div class="form-text text-muted">Add a one month service credit to the customer account</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-info">
|
|
<small>
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
<strong>Note:</strong> This will provision the service on the ONT and link it to the Splynx customer.
|
|
</small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
|
|
<!-- Step 1 buttons -->
|
|
<div id="step1Buttons">
|
|
<button type="button" class="btn btn-primary px-4" id="nextBtn" onclick="fetchCustomerDetails()">
|
|
<i class="bi bi-arrow-right me-2"></i>Next
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Step 2 buttons -->
|
|
<div id="step2Buttons" class="d-none">
|
|
<button type="button" class="btn btn-outline-secondary px-4" id="backBtn" onclick="goBackToStep1()">
|
|
<i class="bi bi-arrow-left me-2"></i>Back
|
|
</button>
|
|
<button type="button" class="btn btn-primary px-4" id="provisionBtn" onclick="provisionService()">
|
|
<i class="bi bi-plus-circle me-2"></i>Provision Service
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Success Modal -->
|
|
<div class="modal fade" id="provisionSuccessModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-check-circle me-2"></i>Service Provisioned Successfully
|
|
</h5>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-center py-3">
|
|
<i class="bi bi-check-circle-fill text-success" style="font-size: 3rem;"></i>
|
|
<h6 class="mt-3 mb-3 text-dark">Success!</h6>
|
|
<div id="successMessage" class="text-dark">
|
|
Service has been provisioned successfully.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer justify-content-center">
|
|
<button type="button" class="btn btn-primary px-4" onclick="closeSuccessModal()">
|
|
<i class="bi bi-x-lg me-2"></i>Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Complex Swap ONT Modal -->
|
|
<div class="modal fade" id="complexSwapModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header text-dark">
|
|
<h5 class="modal-title">Complex ONT Swap</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form method="POST" action="/location/ID/{{ ont['Location_ID'] }}/complex_swap" id="complexSwapForm">
|
|
<input type="hidden" name="ont_id" value="{{ ont['Location_ID'] }}">
|
|
|
|
<div class="mb-3">
|
|
<label for="ont_model" class="form-label text-dark">ONT Model</label>
|
|
<select class="form-select" id="ont_model" name="ont_model" required onchange="checkInterfaceCompatibility()">
|
|
<option value="">Select ONT Model...</option>
|
|
{% for model in ont_models %}
|
|
<option value="{{ model.id }}" data-interface-count="{{ model.InterfaceCount }}">{{ model.Model }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="serial_number" class="form-label text-dark">ONT Serial Number</label>
|
|
<input type="text" class="form-control font-monospace" id="serial_number" name="serial_number"
|
|
placeholder="ADTN24025698" required>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="mac_address" class="form-label text-dark">ONT MAC Address</label>
|
|
<input type="text" class="form-control font-monospace" id="mac_address" name="mac_address"
|
|
placeholder="AA:BB:CC:DD:EE:FF" required>
|
|
</div>
|
|
|
|
<!-- Interface Check Results -->
|
|
<div id="interfaceCheckResult" class="mb-3 d-none">
|
|
<!-- Results will be populated by JavaScript -->
|
|
</div>
|
|
|
|
<div class="alert alert-info">
|
|
<small><strong>Note:</strong> This will create a job that runs asynchronously. You will be redirected to monitor the progress.</small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="submitComplexSwap" onclick="submitComplexSwap()" disabled>
|
|
Create Swap Job
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
|
|
|
|
|
|
function resetTempServiceModal() {
|
|
// Reset to step 1
|
|
document.getElementById('tempStep1').classList.remove('d-none');
|
|
document.getElementById('tempStep2').classList.add('d-none');
|
|
document.getElementById('tempStep1Buttons').classList.remove('d-none');
|
|
document.getElementById('tempStep2Buttons').classList.add('d-none');
|
|
|
|
// Clear form data
|
|
const tempLookupInput = document.getElementById('temp_lookup_splynx_id');
|
|
if (tempLookupInput) {
|
|
tempLookupInput.value = '';
|
|
}
|
|
document.getElementById('tempCustomerDetails').innerHTML = '';
|
|
|
|
// Reset the form
|
|
const tempServiceForm = document.getElementById('tempServiceForm');
|
|
if (tempServiceForm) {
|
|
tempServiceForm.reset();
|
|
}
|
|
|
|
// Hide loading and error states
|
|
document.getElementById('tempLoading').classList.add('d-none');
|
|
document.getElementById('tempCustomerError').classList.add('d-none');
|
|
|
|
// Re-enable next button
|
|
document.getElementById('tempNextBtn').disabled = false;
|
|
}
|
|
|
|
function fetchTempCustomerDetails() {
|
|
const splynxIdElement = document.getElementById('temp_lookup_splynx_id');
|
|
const splynxId = splynxIdElement ? splynxIdElement.value : '';
|
|
|
|
// Clear previous errors
|
|
document.getElementById('tempCustomerError').classList.add('d-none');
|
|
|
|
if (!splynxId || splynxId.trim() === '' || splynxId.trim() === '0') {
|
|
showTempError('Please enter a valid Splynx Customer ID');
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
document.getElementById('tempLoading').classList.remove('d-none');
|
|
document.getElementById('tempNextBtn').disabled = true;
|
|
|
|
const apiUrl = `/api/splynx/customer/${splynxId.trim()}/`;
|
|
|
|
// Make API call
|
|
fetch(apiUrl)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Hide loading
|
|
document.getElementById('tempLoading').classList.add('d-none');
|
|
document.getElementById('tempNextBtn').disabled = false;
|
|
|
|
if (data && data.id) {
|
|
displayTempCustomerDetails(data);
|
|
goToTempStep2();
|
|
} else {
|
|
showTempError('Customer not found or invalid data received');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching customer:', error);
|
|
document.getElementById('tempLoading').classList.add('d-none');
|
|
document.getElementById('tempNextBtn').disabled = false;
|
|
showTempError(`Failed to fetch customer details: ${error.message}`);
|
|
});
|
|
}
|
|
|
|
function displayTempCustomerDetails(customer) {
|
|
const detailsHtml = `
|
|
<div class="row g-2">
|
|
<div class="col-6">
|
|
<strong>Name:</strong><br>
|
|
<span class="text-dark">${customer.name || 'N/A'}</span>
|
|
</div>
|
|
<div class="col-6">
|
|
<strong>Customer ID:</strong><br>
|
|
<span class="badge bg-secondary px-2 py-1">${customer.id}</span>
|
|
</div>
|
|
<div class="col-6">
|
|
<strong>Status:</strong><br>
|
|
${customer.status === 'active'
|
|
? '<span class="badge bg-success px-2 py-1">Active</span>'
|
|
: `<span class="badge bg-warning text-dark px-2 py-1">${customer.status || 'Unknown'}</span>`
|
|
}
|
|
</div>
|
|
<div class="col-6">
|
|
<strong>Email:</strong><br>
|
|
<span class="text-dark">${customer.email || 'N/A'}</span>
|
|
</div>
|
|
<div class="col-12">
|
|
<strong>Address:</strong><br>
|
|
<span class="text-dark">${customer.street_1 || ''} ${customer.street_2 || ''}<br>
|
|
${customer.city || ''} ${customer.zip_code || ''}</span>
|
|
</div>
|
|
<div class="col-6">
|
|
<strong>Phone:</strong><br>
|
|
<span class="text-dark">${customer.phone || 'N/A'}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('tempCustomerDetails').innerHTML = detailsHtml;
|
|
document.getElementById('temp_confirmed_splynx_id').value = customer.id;
|
|
}
|
|
|
|
function showTempError(message) {
|
|
document.getElementById('tempErrorMessage').textContent = message;
|
|
document.getElementById('tempCustomerError').classList.remove('d-none');
|
|
}
|
|
|
|
function goToTempStep2() {
|
|
// Hide step 1, show step 2
|
|
document.getElementById('tempStep1').classList.add('d-none');
|
|
document.getElementById('tempStep2').classList.remove('d-none');
|
|
|
|
// Switch buttons
|
|
document.getElementById('tempStep1Buttons').classList.add('d-none');
|
|
document.getElementById('tempStep2Buttons').classList.remove('d-none');
|
|
}
|
|
|
|
function goBackToTempStep1() {
|
|
// Show step 1, hide step 2
|
|
document.getElementById('tempStep1').classList.remove('d-none');
|
|
document.getElementById('tempStep2').classList.add('d-none');
|
|
|
|
// Switch buttons
|
|
document.getElementById('tempStep1Buttons').classList.remove('d-none');
|
|
document.getElementById('tempStep2Buttons').classList.add('d-none');
|
|
|
|
// Clear any errors
|
|
document.getElementById('tempCustomerError').classList.add('d-none');
|
|
}
|
|
|
|
function provisionTempService() {
|
|
const form = document.getElementById('tempServiceForm');
|
|
const formData = new FormData(form);
|
|
|
|
// Validate required fields
|
|
const uniInterface = document.getElementById('temp_uni_interface').value;
|
|
|
|
if (!uniInterface) {
|
|
alert('Please select a UNI interface');
|
|
return;
|
|
}
|
|
|
|
// Disable provision button to prevent double-clicks
|
|
const tempProvisionBtn = document.getElementById('tempProvisionBtn');
|
|
const originalText = tempProvisionBtn.innerHTML;
|
|
tempProvisionBtn.disabled = true;
|
|
tempProvisionBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Creating...';
|
|
|
|
// Submit the form to the original endpoint
|
|
fetch(`/location/ID/{{ ont['Location_ID'] }}/tempService`, {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('temp service provisioning data:', data);
|
|
if (data.success) {
|
|
// Show success modal instead of alert
|
|
showTempSuccessModal(data.message || 'Temporary internet service has been created successfully.');
|
|
} else {
|
|
throw new Error(data.message || 'Failed to create temporary service');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error creating temp service:', error);
|
|
alert(`Failed to create temporary service: ${error.message}`);
|
|
|
|
// Re-enable button on error
|
|
tempProvisionBtn.disabled = false;
|
|
tempProvisionBtn.innerHTML = originalText;
|
|
});
|
|
// Note: Button re-enabling on success is handled by page refresh
|
|
}
|
|
|
|
function closeTempSuccessModal() {
|
|
// Close the success modal and refresh the page
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('tempServiceSuccessModal'));
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
|
|
// Add a small delay to ensure modal closes gracefully
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 300);
|
|
}
|
|
|
|
function showTempSuccessModal(message) {
|
|
// Set the success message
|
|
document.getElementById('tempSuccessMessage').innerHTML = message;
|
|
|
|
// Hide the temp service modal first
|
|
const tempServiceModal = bootstrap.Modal.getInstance(document.getElementById('newTempServiceModal'));
|
|
if (tempServiceModal) {
|
|
tempServiceModal.hide();
|
|
}
|
|
|
|
// Show success modal after a brief delay to ensure temp service modal is hidden
|
|
setTimeout(() => {
|
|
const successModal = new bootstrap.Modal(document.getElementById('tempServiceSuccessModal'), {
|
|
backdrop: 'static',
|
|
keyboard: false
|
|
});
|
|
successModal.show();
|
|
}, 300);
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetProvisionModal() {
|
|
// Reset to step 1
|
|
document.getElementById('step1').classList.remove('d-none');
|
|
document.getElementById('step2').classList.add('d-none');
|
|
document.getElementById('step1Buttons').classList.remove('d-none');
|
|
document.getElementById('step2Buttons').classList.add('d-none');
|
|
|
|
// Clear form data - be specific about which elements to clear
|
|
const lookupInput = document.getElementById('lookup_splynx_id');
|
|
if (lookupInput) {
|
|
lookupInput.value = '';
|
|
}
|
|
document.getElementById('customerDetails').innerHTML = '';
|
|
|
|
// Only reset the actual form, not the input outside of it
|
|
const provisionForm = document.getElementById('provisionForm');
|
|
if (provisionForm) {
|
|
provisionForm.reset();
|
|
}
|
|
|
|
// Reset VLAN option to untagged and hide VLAN ID field
|
|
const untaggedRadio = document.getElementById('vlan_untagged');
|
|
if (untaggedRadio) {
|
|
untaggedRadio.checked = true;
|
|
handleVlanOptionChange();
|
|
}
|
|
|
|
// Hide loading and error states
|
|
document.getElementById('loading').classList.add('d-none');
|
|
document.getElementById('customerError').classList.add('d-none');
|
|
|
|
// Re-enable next button
|
|
document.getElementById('nextBtn').disabled = false;
|
|
}
|
|
|
|
function handleVlanOptionChange() {
|
|
const vlanContainer = document.getElementById('vlan_id_container');
|
|
const vlanIdInput = document.getElementById('vlan_id');
|
|
const untaggedSelected = document.getElementById('vlan_untagged').checked;
|
|
|
|
if (untaggedSelected) {
|
|
vlanContainer.style.display = 'none';
|
|
vlanIdInput.required = false;
|
|
vlanIdInput.value = '';
|
|
} else {
|
|
vlanContainer.style.display = 'block';
|
|
vlanIdInput.required = true;
|
|
}
|
|
}
|
|
|
|
function fetchCustomerDetails() {
|
|
const splynxIdElement = document.getElementById('lookup_splynx_id');
|
|
const splynxId = splynxIdElement ? splynxIdElement.value : '';
|
|
|
|
// Clear previous errors
|
|
document.getElementById('customerError').classList.add('d-none');
|
|
|
|
if (!splynxId || splynxId.trim() === '' || splynxId.trim() === '0') {
|
|
showError('Please enter a valid Splynx Customer ID');
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
document.getElementById('loading').classList.remove('d-none');
|
|
document.getElementById('nextBtn').disabled = true;
|
|
|
|
const apiUrl = `/api/splynx/customer/${splynxId.trim()}/`;
|
|
|
|
// Make API call
|
|
fetch(apiUrl)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Hide loading
|
|
document.getElementById('loading').classList.add('d-none');
|
|
document.getElementById('nextBtn').disabled = false;
|
|
|
|
if (data && data.id) {
|
|
displayCustomerDetails(data);
|
|
goToStep2();
|
|
} else {
|
|
showError('Customer not found or invalid data received');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching customer:', error);
|
|
document.getElementById('loading').classList.add('d-none');
|
|
document.getElementById('nextBtn').disabled = false;
|
|
showError(`Failed to fetch customer details: ${error.message}`);
|
|
});
|
|
}
|
|
|
|
function displayCustomerDetails(customer) {
|
|
const detailsHtml = `
|
|
<div class="row g-2">
|
|
<div class="col-6">
|
|
<strong>Name:</strong><br>
|
|
<span class="text-dark">${customer.name || 'N/A'}</span>
|
|
</div>
|
|
<div class="col-6">
|
|
<strong>Customer ID:</strong><br>
|
|
<span class="badge bg-secondary px-2 py-1">${customer.id}</span>
|
|
</div>
|
|
<div class="col-6">
|
|
<strong>Status:</strong><br>
|
|
${customer.status === 'active'
|
|
? '<span class="badge bg-success px-2 py-1">Active</span>'
|
|
: `<span class="badge bg-warning text-dark px-2 py-1">${customer.status || 'Unknown'}</span>`
|
|
}
|
|
</div>
|
|
<div class="col-6">
|
|
<strong>Email:</strong><br>
|
|
<span class="text-dark">${customer.email || 'N/A'}</span>
|
|
</div>
|
|
<div class="col-12">
|
|
<strong>Address:</strong><br>
|
|
<span class="text-dark">${customer.street_1 || ''} ${customer.street_2 || ''}<br>
|
|
${customer.city || ''} ${customer.zip_code || ''}</span>
|
|
</div>
|
|
<div class="col-6">
|
|
<strong>Phone:</strong><br>
|
|
<span class="text-dark">${customer.phone || 'N/A'}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('customerDetails').innerHTML = detailsHtml;
|
|
document.getElementById('confirmed_splynx_id').value = customer.id;
|
|
}
|
|
|
|
function showError(message) {
|
|
document.getElementById('errorMessage').textContent = message;
|
|
document.getElementById('customerError').classList.remove('d-none');
|
|
}
|
|
|
|
function goToStep2() {
|
|
// Hide step 1, show step 2
|
|
document.getElementById('step1').classList.add('d-none');
|
|
document.getElementById('step2').classList.remove('d-none');
|
|
|
|
// Switch buttons
|
|
document.getElementById('step1Buttons').classList.add('d-none');
|
|
document.getElementById('step2Buttons').classList.remove('d-none');
|
|
}
|
|
|
|
function goBackToStep1() {
|
|
// Show step 1, hide step 2
|
|
document.getElementById('step1').classList.remove('d-none');
|
|
document.getElementById('step2').classList.add('d-none');
|
|
|
|
// Switch buttons
|
|
document.getElementById('step1Buttons').classList.remove('d-none');
|
|
document.getElementById('step2Buttons').classList.add('d-none');
|
|
|
|
// Clear any errors
|
|
document.getElementById('customerError').classList.add('d-none');
|
|
}
|
|
|
|
function provisionService() {
|
|
const form = document.getElementById('provisionForm');
|
|
const formData = new FormData(form);
|
|
|
|
// Validate required fields
|
|
const planId = document.getElementById('plan_id').value;
|
|
const uniInterface = document.getElementById('uni_interface').value;
|
|
const vlanOption = document.querySelector('input[name="vlan_option"]:checked').value;
|
|
const vlanId = document.getElementById('vlan_id').value;
|
|
|
|
if (!planId) {
|
|
alert('Please select a service plan');
|
|
return;
|
|
}
|
|
|
|
if (!uniInterface) {
|
|
alert('Please select a UNI interface');
|
|
return;
|
|
}
|
|
|
|
if (vlanOption === 'tagged' && (!vlanId || vlanId < 2 || vlanId > 4096)) {
|
|
alert('Please enter a valid VLAN ID (2-4096) for tagged VLAN');
|
|
return;
|
|
}
|
|
|
|
// Add VLAN info to form data
|
|
if (vlanOption === 'untagged') {
|
|
formData.append('vlan_type', 'untagged');
|
|
} else {
|
|
formData.append('vlan_type', 'tagged');
|
|
formData.append('vlan_id', vlanId);
|
|
}
|
|
|
|
// Disable provision button to prevent double-clicks
|
|
const provisionBtn = document.getElementById('provisionBtn');
|
|
const originalText = provisionBtn.innerHTML;
|
|
provisionBtn.disabled = true;
|
|
provisionBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Provisioning...';
|
|
|
|
// Submit the form
|
|
fetch('/service/provision', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('provisioning data:', data);
|
|
if (data.success) {
|
|
// Show success modal instead of alert
|
|
showSuccessModal(data.message || 'Service has been provisioned successfully.');
|
|
} else {
|
|
throw new Error(data.message || 'Failed to provision service');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error provisioning service:', error);
|
|
alert(`Failed to provision service: ${error.message}`);
|
|
|
|
// Re-enable button on error
|
|
provisionBtn.disabled = false;
|
|
provisionBtn.innerHTML = originalText;
|
|
});
|
|
// Note: Button re-enabling on success is handled by page refresh
|
|
}
|
|
|
|
function closeSuccessModal() {
|
|
// Close the success modal and refresh the page
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('provisionSuccessModal'));
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
|
|
// Add a small delay to ensure modal closes gracefully
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 300);
|
|
}
|
|
|
|
function showSuccessModal(message) {
|
|
// Set the success message
|
|
document.getElementById('successMessage').innerHTML = message;
|
|
|
|
// Hide the provision modal first
|
|
const provisionModal = bootstrap.Modal.getInstance(document.getElementById('provisionServiceModal'));
|
|
if (provisionModal) {
|
|
provisionModal.hide();
|
|
}
|
|
|
|
// Show success modal after a brief delay to ensure provision modal is hidden
|
|
setTimeout(() => {
|
|
const successModal = new bootstrap.Modal(document.getElementById('provisionSuccessModal'), {
|
|
backdrop: 'static',
|
|
keyboard: false
|
|
});
|
|
successModal.show();
|
|
}, 300);
|
|
}
|
|
|
|
// Complex Swap Functions
|
|
function checkInterfaceCompatibility() {
|
|
const ontModelSelect = document.getElementById('ont_model');
|
|
const resultDiv = document.getElementById('interfaceCheckResult');
|
|
const submitBtn = document.getElementById('submitComplexSwap');
|
|
|
|
if (!ontModelSelect.value) {
|
|
resultDiv.classList.add('d-none');
|
|
submitBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
const ontId = {{ ont['Location_ID'] }};
|
|
|
|
// Show loading state
|
|
resultDiv.innerHTML = `
|
|
<div class="alert alert-info">
|
|
<div class="spinner-border spinner-border-sm me-2"></div>
|
|
Checking interface compatibility...
|
|
</div>
|
|
`;
|
|
resultDiv.classList.remove('d-none');
|
|
submitBtn.disabled = true;
|
|
|
|
// Make API call
|
|
fetch(`/api/ont/interface_check/${ontId}/`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.passed) {
|
|
resultDiv.innerHTML = `
|
|
<div class="alert alert-success">
|
|
<i class="bi bi-check-circle me-2"></i>
|
|
Interface compatibility check passed. Ready to proceed.
|
|
</div>
|
|
`;
|
|
submitBtn.disabled = false;
|
|
} else {
|
|
resultDiv.innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
<strong>Compatibility Issue:</strong> ${data.reason || 'Interface check failed'}
|
|
</div>
|
|
`;
|
|
submitBtn.disabled = true;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Interface check error:', error);
|
|
resultDiv.innerHTML = `
|
|
<div class="alert alert-warning">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
Unable to verify interface compatibility. Please check manually.
|
|
</div>
|
|
`;
|
|
submitBtn.disabled = false; // Allow user to proceed at their own risk
|
|
});
|
|
}
|
|
|
|
function submitComplexSwap() {
|
|
const form = document.getElementById('complexSwapForm');
|
|
const submitBtn = document.getElementById('submitComplexSwap');
|
|
|
|
// Validate form
|
|
if (!form.checkValidity()) {
|
|
form.reportValidity();
|
|
return;
|
|
}
|
|
|
|
// Disable button and show loading
|
|
const originalText = submitBtn.innerHTML;
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Creating Job...';
|
|
|
|
// Submit form
|
|
form.submit();
|
|
}
|
|
</script>
|
|
|
|
{% endblock %}
|