2025-05-16 10:18:55 +04:00

843 lines
37 KiB
HTML

{% extends "base.html" %}
{% block title %}Results - SCADA vs DWG Manifest Comparison Tool{% endblock %}
{% block extra_css %}
<style>
.missing-item {
background-color: #ffebee;
}
/* Main container for the table */
.table-container {
position: relative;
min-height: 200px;
border-top: none;
overflow: visible; /* Allow the inner content to determine scrolling */
}
/* Sticky header at the top */
.sticky-table-header {
position: sticky;
top: 0;
z-index: 20;
background-color: white;
border-bottom: 1px solid #dee2e6;
width: 100%;
}
/* Scrollable content area */
.scrollable-table-content {
overflow-y: auto;
max-height: 500px; /* Only this element should have scrolling */
min-height: 200px;
scrollbar-width: thin; /* For Firefox */
padding-bottom: 10px; /* Add some padding at bottom to ensure last row is visible */
}
/* For Webkit browsers like Chrome/Safari */
.scrollable-table-content::-webkit-scrollbar {
width: 8px;
}
/* Table filter inside sticky header */
.table-filter-container {
padding: 10px;
background-color: white;
border-bottom: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* Fixed table layout - prevents column width changes */
.table-fixed {
table-layout: fixed;
width: 100%;
}
.table-fixed td, .table-fixed th {
word-wrap: break-word;
padding: 8px;
}
/* Make table header sticky */
.scrollable-table-content thead {
position: sticky;
top: 0;
z-index: 10;
background-color: white;
width: 100%; /* Full width */
}
.scrollable-table-content thead tr {
display: table;
width: 100%;
table-layout: fixed;
}
.scrollable-table-content thead th {
background-color: white;
border-bottom: 2px solid #dee2e6;
box-shadow: 0 2px 5px rgba(0,0,0,0.15);
font-weight: bold;
padding: 10px 8px;
}
/* Remove border-spacing */
.scrollable-table-content table {
width: 100%;
border-collapse: collapse;
table-layout: fixed; /* Force fixed layout */
}
.summary-box {
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
}
.bg-light-primary {
background-color: #e3f2fd;
}
/* Make sure the table header has a white background that fully covers content */
.table > thead {
background-color: white;
}
/* Copy button styling */
.copy-btn {
min-width: 80px;
}
/* Additional styling to ensure header is fully visible */
.scrollable-table-content thead::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
z-index: -1;
}
/* Fixed column layout */
.name-col {
width: 70%;
}
.control-panel-col {
width: 30%;
text-align: right;
}
/* Table structure */
.comparison-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed; /* Force fixed layout */
}
.comparison-table th:first-child,
.comparison-table td:first-child {
text-align: left;
padding-right: 10px;
}
.comparison-table th:last-child,
.comparison-table td:last-child {
text-align: right;
padding-left: 10px;
}
.comparison-table tr {
border-bottom: 1px solid #e0e0e0;
}
/* Ensure rows maintain proper layout */
.table tbody tr {
display: table;
width: 100%;
table-layout: fixed;
}
/* Reset any overflow settings on card containers */
.card, .card-body {
overflow: visible !important;
}
/* Main container for the table */
.table-container {
position: relative;
min-height: 200px;
border-top: none;
overflow: visible;
max-height: none;
}
/* Scrollable content area - ONLY this should have scrolling */
.scrollable-table-content {
overflow-y: auto;
max-height: 500px;
min-height: 200px;
scrollbar-width: thin; /* For Firefox */
padding-bottom: 10px;
}
/* Ensure parent containers don't create scrollbars */
.card, .card-body, .card-header, .card-footer {
overflow: visible !important;
}
/* Remove height constraints from parent containers */
.card.h-100 {
height: auto !important;
min-height: auto !important;
max-height: none !important;
}
/* Fixed table with scrollable body */
.table-container {
position: relative;
border-top: none;
overflow: hidden;
}
/* Set the table header to be sticky */
.table-fixed thead {
position: sticky;
top: 0;
z-index: 10;
background-color: white;
}
/* Make only the tbody scroll */
.scrollable-table-content {
overflow-y: auto;
max-height: 400px;
scrollbar-width: thin;
}
/* Remove any height constraints from parent cards */
.card.h-100 {
height: auto !important;
}
/* Ensure the table fills its container */
.table-fixed {
width: 100%;
margin-bottom: 0;
}
/* Give header cells proper styling */
.table-fixed th {
background-color: white;
position: sticky;
top: 0;
z-index: 10;
border-bottom: 2px solid #dee2e6;
}
/* COMPLETELY REWRITTEN TABLE STYLES */
/* Basic reset - clear previous styles */
.table-container, .scrollable-table-content, .card.h-100 {
max-height: none;
min-height: 0;
overflow: visible;
height: auto !important;
}
/* Table container - only for positioning */
.table-container {
position: relative;
border-top: none;
}
/* Scrollable content container - ONLY place with scrollbar */
.scrollable-table-content {
display: block;
width: 100%;
overflow-y: auto;
max-height: 400px; /* Adjust height as needed */
border: 1px solid #dee2e6;
}
/* Ensure table takes full width */
.table-fixed {
width: 100%;
margin-bottom: 0;
border-collapse: separate;
border-spacing: 0;
}
/* Make header stick to top */
.table-fixed thead {
position: sticky;
top: 0;
z-index: 5;
}
/* Header styling */
.table-fixed th {
background-color: #fff;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
border-bottom: 2px solid #dee2e6;
position: sticky;
top: 0;
}
/* Fix for last row being cut off */
.scrollable-table-content tbody tr:last-child td {
border-bottom: 1px solid #dee2e6;
}
/* Add bottom padding to ensure all content visible */
.scrollable-table-content {
padding-bottom: 1px;
}
</style>
{% endblock %}
{% block content %}
<!-- Store name mappings for JavaScript -->
<script>
// Store name mappings as a JavaScript object
window.nameMappings = {};
{% if data.name_mappings and data.name_mappings.scada %}
window.nameMappings.scada = {};
{% for key, value in data.name_mappings.scada.items() %}
window.nameMappings.scada["{{ key }}"] = {
originalName: {{ value.original_name|tojson }},
controlPanel: {{ value.control_panel|tojson }}
};
{% endfor %}
{% else %}
window.nameMappings.scada = {};
{% endif %}
{% if data.name_mappings and data.name_mappings.manifest %}
window.nameMappings.manifest = {};
{% for key, value in data.name_mappings.manifest.items() %}
window.nameMappings.manifest["{{ key }}"] = {
originalName: {{ value.original_name|tojson }},
controlPanel: {{ value.control_panel|tojson }}
};
{% endfor %}
{% else %}
window.nameMappings.manifest = {};
{% endif %}
{% if data.name_mappings and data.name_mappings.dwg %}
window.nameMappings.dwg = {};
{% for key, value in data.name_mappings.dwg.items() %}
window.nameMappings.dwg["{{ key }}"] = {
originalName: {{ value.original_name|tojson }},
controlPanel: {{ value.control_panel|tojson }}
};
{% endfor %}
{% else %}
window.nameMappings.dwg = {};
{% endif %}
</script>
<!-- Comparison Selector -->
{% if comparisons %}
<div class="card mb-4">
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0">Available Comparisons</h4>
<a href="{{ url_for('index') }}" class="btn btn-light btn-sm">Back to Upload</a>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<select id="comparison-selector" class="form-select form-select-lg mb-3">
<option value="">-- Select a different comparison --</option>
{% for comparison_id, comparison in comparisons.items() %}
<option value="{{ comparison_id }}" {% if comparison_id == data.comparison_id %}selected{% endif %}>
{{ comparison.name }} ({{ comparison.timestamp }})
</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<div class="d-grid gap-2">
<button id="view-comparison-btn" class="btn btn-success">
<i class="fas fa-chart-bar"></i> View Selected Comparison
</button>
</div>
</div>
</div>
<div id="comparison-actions" class="row mt-3">
<div class="col-md-8">
<div class="input-group">
<input type="text" id="comparison-name" class="form-control" placeholder="Rename comparison" value="{{ data.name }}">
<button id="rename-comparison-btn" class="btn btn-outline-secondary">
<i class="fas fa-edit"></i> Rename
</button>
</div>
</div>
<div class="col-md-4">
<div class="d-grid gap-2">
<button id="delete-comparison-btn" class="btn btn-danger">
<i class="fas fa-trash"></i> Delete Comparison
</button>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Comparison Results: {{ data.name }}</h4>
</div>
<div class="card-body">
<!-- Summary Statistics -->
<div class="summary-box bg-light-primary mb-4">
<div class="row">
<div class="col-md-4">
<h5>SCADA Items: {{ data.scada_vs_manifest.scada_count }}</h5>
</div>
<div class="col-md-4">
<h5>Manifest Items: {{ data.scada_vs_manifest.manifest_count }}</h5>
</div>
<div class="col-md-4">
<h5>DWG Items: {{ data.scada_vs_dwg.dwg_count }}</h5>
</div>
</div>
</div>
<!-- Repository Information -->
<div class="summary-box bg-light mb-4">
<div class="row">
<div class="col-md-6">
<h5>Repository: {{ data.repository_url }}</h5>
<p>Last updated: {{ data.timestamp }}</p>
</div>
<div class="col-md-6 text-end">
<form id="refresh-repo-form" action="{{ url_for('refresh_repository') }}" method="post">
<input type="hidden" name="repo_id" value="{{ data.repo_id }}">
<input type="hidden" name="comparison_id" value="{{ data.comparison_id }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-primary">
<i class="fas fa-sync-alt"></i> Refresh Repository & Reload Data
</button>
</form>
</div>
</div>
</div>
<!-- Tabs Navigation -->
<ul class="nav nav-tabs" id="comparisonTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="scada-dwg-tab" data-bs-toggle="tab" data-bs-target="#scada-dwg" type="button" role="tab" aria-controls="scada-dwg" aria-selected="true">SCADA vs DWG</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="manifest-dwg-tab" data-bs-toggle="tab" data-bs-target="#manifest-dwg" type="button" role="tab" aria-controls="manifest-dwg" aria-selected="false">Manifest vs DWG</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content mt-3" id="comparisonTabsContent">
<!-- SCADA vs DWG Tab -->
<div class="tab-pane fade show active" id="scada-dwg" role="tabpanel" aria-labelledby="scada-dwg-tab">
<div class="row">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-danger text-white">
<h5 class="mb-0">In SCADA but Missing from DWG ({{ data.scada_vs_dwg.only_in_scada|length }})</h5>
</div>
<div class="card-body table-container">
{% if data.scada_vs_dwg.only_in_scada %}
<div class="scrollable-table-content">
<table class="table table-striped table-bordered table-fixed">
<thead>
<tr>
<th class="name-col">Name</th>
<th class="control-panel-col">Control Panel</th>
</tr>
</thead>
<tbody>
{% for item in data.scada_vs_dwg.only_in_scada %}
<tr class="missing-item">
<td class="name-col">{{ item.name }}</td>
<td class="control-panel-col">{{ item.control_panel }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-success">No items in SCADA are missing from DWG.</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-danger text-white">
<h5 class="mb-0">In DWG but Missing from SCADA ({{ data.scada_vs_dwg.only_in_dwg|length }})</h5>
</div>
<div class="card-body table-container">
{% if data.scada_vs_dwg.only_in_dwg %}
<div class="scrollable-table-content">
<table class="table table-striped table-bordered table-fixed">
<thead>
<tr>
<th class="name-col">Name</th>
<th class="control-panel-col">Control Panel</th>
</tr>
</thead>
<tbody>
{% for item in data.scada_vs_dwg.only_in_dwg %}
<tr class="missing-item">
<td class="name-col">{{ item.name }}</td>
<td class="control-panel-col">{{ item.control_panel }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-success">No items in DWG are missing from SCADA.</p>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0">Names Found in Both SCADA and DWG ({{ data.scada_vs_dwg.common|length }})</h5>
</div>
<div class="card-body table-container">
{% if data.scada_vs_dwg.common %}
<div class="scrollable-table-content">
<table class="table table-striped table-bordered table-fixed">
<thead>
<tr>
<th class="name-col">Name</th>
<th class="control-panel-col">Control Panel</th>
</tr>
</thead>
<tbody>
{% for item in data.scada_vs_dwg.common %}
<tr>
<td class="name-col">{{ item.name }}</td>
<td class="control-panel-col">{{ item.control_panel }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-danger">No common names found between SCADA and DWG.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Manifest vs DWG Tab -->
<div class="tab-pane fade" id="manifest-dwg" role="tabpanel" aria-labelledby="manifest-dwg-tab">
<div class="row">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-danger text-white">
<h5 class="mb-0">In Manifest but Missing from DWG ({{ data.manifest_vs_dwg.only_in_manifest|length }})</h5>
</div>
<div class="card-body table-container">
{% if data.manifest_vs_dwg.only_in_manifest %}
<div class="scrollable-table-content">
<table class="table table-striped table-bordered table-fixed">
<thead>
<tr>
<th class="name-col">Name</th>
</tr>
</thead>
<tbody>
{% for item in data.manifest_vs_dwg.only_in_manifest %}
<tr class="missing-item">
<td class="name-col">{{ item.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-success">No items in Manifest are missing from DWG.</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-danger text-white">
<h5 class="mb-0">In DWG but Missing from Manifest ({{ data.manifest_vs_dwg.only_in_dwg|length }})</h5>
</div>
<div class="card-body table-container">
{% if data.manifest_vs_dwg.only_in_dwg %}
<div class="scrollable-table-content">
<table class="table table-striped table-bordered table-fixed">
<thead>
<tr>
<th class="name-col">Name</th>
</tr>
</thead>
<tbody>
{% for item in data.manifest_vs_dwg.only_in_dwg %}
<tr class="missing-item">
<td class="name-col">{{ item.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-success">No items in DWG are missing from Manifest.</p>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0">Names Found in Both Manifest and DWG ({{ data.manifest_vs_dwg.common|length }})</h5>
</div>
<div class="card-body table-container">
{% if data.manifest_vs_dwg.common %}
<div class="scrollable-table-content">
<table class="table table-striped table-bordered table-fixed">
<thead>
<tr>
<th class="name-col">Name</th>
</tr>
</thead>
<tbody>
{% for item in data.manifest_vs_dwg.common %}
<tr>
<td class="name-col">{{ item.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-danger">No common names found between Manifest and DWG.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<h5>Update Comparison</h5>
<form id="update-comparison-form" action="{{ url_for('update_files') }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="repo_id" value="{{ data.repo_id }}">
<input type="hidden" name="comparison_id" value="{{ data.comparison_id }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="manifest_file" class="form-label">New Manifest Excel File (Optional)</label>
<input type="file" class="form-control" id="manifest_file" name="manifest_file" accept=".xlsx">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="dwg_file" class="form-label">New DWG Excel File (Optional)</label>
<input type="file" class="form-control" id="dwg_file" name="dwg_file" accept=".xlsx">
</div>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Update Comparison</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Store CSRF token for JavaScript use
const csrfToken = "{{ csrf_token() }}";
// Initialize the Bootstrap tabs
document.addEventListener('DOMContentLoaded', function() {
// This ensures tabs work properly
var triggerTabList = [].slice.call(document.querySelectorAll('#comparisonTabs button'))
triggerTabList.forEach(function(triggerEl) {
new bootstrap.Tab(triggerEl);
});
// Handle the refresh repository form submission
const refreshForm = document.getElementById('refresh-repo-form');
if (refreshForm) {
refreshForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
// Update button text and disable
const submitBtn = this.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing...';
fetch(this.action, {
method: 'POST',
body: formData,
redirect: 'manual'
})
.then(response => {
if (response.type === 'opaqueredirect') {
window.location.href = response.url || '/';
return;
}
return response.text();
})
.then(html => {
if (html) {
// If we got HTML back, refresh the page
window.location.reload();
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while refreshing the repository. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnText;
});
});
}
// Handle the update comparison form submission
const updateForm = document.getElementById('update-comparison-form');
if (updateForm) {
updateForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
// Show loading indicator
const submitBtn = this.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = 'Updating...';
fetch(this.action, {
method: 'POST',
body: formData,
redirect: 'manual'
})
.then(response => {
if (response.type === 'opaqueredirect') {
// Server redirected, follow the redirect
window.location.href = response.url || '/';
return;
}
return response.text();
})
.then(html => {
if (html) {
// If we got HTML back and not a redirect, handle it
window.location.reload();
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred during update. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnText;
});
});
}
// Comparison selector functionality
const comparisonSelector = document.getElementById('comparison-selector');
const viewComparisonBtn = document.getElementById('view-comparison-btn');
const comparisonActions = document.getElementById('comparison-actions');
const comparisonNameInput = document.getElementById('comparison-name');
const renameComparisonBtn = document.getElementById('rename-comparison-btn');
const deleteComparisonBtn = document.getElementById('delete-comparison-btn');
if (viewComparisonBtn) {
viewComparisonBtn.addEventListener('click', function() {
const selectedValue = comparisonSelector.value;
if (selectedValue) {
window.location.href = '/comparison/' + selectedValue;
}
});
}
if (renameComparisonBtn) {
renameComparisonBtn.addEventListener('click', function() {
const selectedValue = comparisonSelector.value || '{{ data.comparison_id }}';
const newName = comparisonNameInput.value.trim();
if (selectedValue && newName) {
fetch('/rename_comparison/' + selectedValue, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrfToken
},
body: 'name=' + encodeURIComponent(newName)
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update the option text
if (comparisonSelector) {
const selectedOption = comparisonSelector.options[comparisonSelector.selectedIndex];
const timestampPart = selectedOption.text.split(' (')[1];
selectedOption.text = newName + ' (' + timestampPart;
}
// Update the page title
const headerTitle = document.querySelector('.card-header h4');
if (headerTitle) {
headerTitle.textContent = 'Comparison Results: ' + newName;
}
alert('Comparison renamed successfully!');
} else {
alert('Failed to rename comparison: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while renaming the comparison.');
});
}
});
}
if (deleteComparisonBtn) {
deleteComparisonBtn.addEventListener('click', function() {
const selectedValue = comparisonSelector ? comparisonSelector.value : '{{ data.comparison_id }}';
if (selectedValue && confirm('Are you sure you want to delete this comparison? This action cannot be undone.')) {
// Use fetch API instead of form submission
fetch('/delete_comparison/' + selectedValue, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
},
redirect: 'manual'
})
.then(response => {
if (response.type === 'opaqueredirect') {
window.location.href = response.url || '/';
return;
}
return response.text();
})
.then(html => {
if (html) {
window.location.href = '/';
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the comparison. Please try again.');
});
}
});
}
});
</script>
{% endblock %}