Compare commits
6 Commits
ac29b6e929
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 211db514dd | |||
| 24971ac5fc | |||
|
|
a1a5f90f00 | ||
| 97995fa58e | |||
| bcd582aee5 | |||
| dc6b551325 |
@@ -397,10 +397,10 @@ tbody tr:hover {
|
|||||||
border-color: transparent #333 transparent transparent;
|
border-color: transparent #333 transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notes-tooltip:hover .tooltip-text {
|
/* .notes-tooltip:hover .tooltip-text {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
} */
|
||||||
|
|
||||||
/* Modal Styles */
|
/* Modal Styles */
|
||||||
.modal {
|
.modal {
|
||||||
|
|||||||
@@ -1129,6 +1129,14 @@
|
|||||||
let currentUserId = null;
|
let currentUserId = null;
|
||||||
let currentChangePasswordUserId = null;
|
let currentChangePasswordUserId = null;
|
||||||
|
|
||||||
|
// ==================== GENERIC MODAL HELPER ====================
|
||||||
|
function closeModal(modalId, additionalCleanup = null) {
|
||||||
|
document.getElementById(modalId).style.display = 'none';
|
||||||
|
if (additionalCleanup) {
|
||||||
|
additionalCleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load UI configuration from API
|
// Load UI configuration from API
|
||||||
async function loadUIConfig() {
|
async function loadUIConfig() {
|
||||||
try {
|
try {
|
||||||
@@ -4433,6 +4441,10 @@
|
|||||||
title: "Today's Pending Departures",
|
title: "Today's Pending Departures",
|
||||||
text: "Displays aircraft that are ready to depart from Swansea today. This includes flights with approved PPRs awaiting departure, local flights that have been booked out, and departures to other airfields. These aircraft will depart today."
|
text: "Displays aircraft that are ready to depart from Swansea today. This includes flights with approved PPRs awaiting departure, local flights that have been booked out, and departures to other airfields. These aircraft will depart today."
|
||||||
},
|
},
|
||||||
|
overflights: {
|
||||||
|
title: "Active Overflights",
|
||||||
|
text: "Displays aircraft that are currently in contact with Air / Ground. Once marked a QSY (changed frequency), they are no longer considered active overflights."
|
||||||
|
},
|
||||||
departed: {
|
departed: {
|
||||||
title: "Departed Today",
|
title: "Departed Today",
|
||||||
text: "Displays the flights which have departed today to other airfields, either by way of a PPR or by booking out."
|
text: "Displays the flights which have departed today to other airfields, either by way of a PPR or by booking out."
|
||||||
@@ -4769,13 +4781,50 @@
|
|||||||
if (lookup) lookup.clear();
|
if (lookup) lookup.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Position tooltip near mouse cursor
|
||||||
|
function positionTooltip(event) {
|
||||||
|
const tooltip = event.currentTarget.querySelector('.tooltip-text');
|
||||||
|
if (tooltip) {
|
||||||
|
const rect = tooltip.getBoundingClientRect();
|
||||||
|
const tooltipWidth = 300; // matches CSS width
|
||||||
|
const tooltipHeight = rect.height || 100; // estimate if not yet rendered
|
||||||
|
|
||||||
|
let left = event.pageX + 10;
|
||||||
|
let top = event.pageY + 10;
|
||||||
|
|
||||||
|
// Adjust if tooltip would go off screen
|
||||||
|
if (left + tooltipWidth > window.innerWidth) {
|
||||||
|
left = event.pageX - tooltipWidth - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top + tooltipHeight > window.innerHeight) {
|
||||||
|
top = event.pageY - tooltipHeight - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip.style.left = left + 'px';
|
||||||
|
tooltip.style.top = top + 'px';
|
||||||
|
tooltip.style.visibility = 'visible';
|
||||||
|
tooltip.style.opacity = '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add hover listeners to all notes tooltips
|
// Add hover listeners to all notes tooltips
|
||||||
function setupTooltips() {
|
function setupTooltips() {
|
||||||
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {
|
document.querySelectorAll('.notes-tooltip').forEach(tooltip => {
|
||||||
tooltip.addEventListener('mouseenter', positionTooltip);
|
tooltip.addEventListener('mouseenter', positionTooltip);
|
||||||
|
tooltip.addEventListener('mouseleave', hideTooltip);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide tooltip when mouse leaves
|
||||||
|
function hideTooltip(event) {
|
||||||
|
const tooltip = event.currentTarget.querySelector('.tooltip-text');
|
||||||
|
if (tooltip) {
|
||||||
|
tooltip.style.visibility = 'hidden';
|
||||||
|
tooltip.style.opacity = '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the page when DOM is loaded
|
// Initialize the page when DOM is loaded
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadUIConfig(); // Load UI configuration first
|
loadUIConfig(); // Load UI configuration first
|
||||||
|
|||||||
27
web/assets/bell.svg
Normal file
27
web/assets/bell.svg
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<svg viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Bell body -->
|
||||||
|
<path d="M 30,40 Q 30,35 35,35 L 65,35 Q 70,35 70,40 Q 70,60 50,70 Q 30,60 30,40" fill="#FFD700" stroke="#DAA520" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Bell shine/highlight -->
|
||||||
|
<ellipse cx="45" cy="45" rx="8" ry="6" fill="#FFED4E" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Bell clapper -->
|
||||||
|
<circle cx="50" cy="65" r="4" fill="#8B4513"/>
|
||||||
|
<path d="M 50,65 Q 48,75 47,85" stroke="#8B4513" stroke-width="2" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Top of bell (rope/hanging part) -->
|
||||||
|
<rect x="47" y="25" width="6" height="10" fill="#DAA520" rx="2"/>
|
||||||
|
|
||||||
|
<!-- Loop -->
|
||||||
|
<path d="M 48,25 Q 40,20 50,15 Q 60,20 52,25" stroke="#DAA520" stroke-width="2" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Decorative berries around bell -->
|
||||||
|
<circle cx="25" cy="50" r="2" fill="#E74C3C"/>
|
||||||
|
<circle cx="75" cy="50" r="2" fill="#E74C3C"/>
|
||||||
|
<circle cx="28" cy="60" r="2" fill="#E74C3C"/>
|
||||||
|
<circle cx="72" cy="60" r="2" fill="#E74C3C"/>
|
||||||
|
|
||||||
|
<!-- Holly leaves -->
|
||||||
|
<path d="M 20,45 L 18,48 L 20,50 L 18,52 L 20,55" stroke="#228B22" stroke-width="1.5" fill="none"/>
|
||||||
|
<path d="M 80,45 L 82,48 L 80,50 L 82,52 L 80,55" stroke="#228B22" stroke-width="1.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
10
web/assets/candycane.svg
Normal file
10
web/assets/candycane.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Candy cane curve -->
|
||||||
|
<path d="M 50,10 Q 30,30 30,60 Q 30,90 50,100" stroke="#E74C3C" stroke-width="12" fill="none" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- White stripe -->
|
||||||
|
<path d="M 50,10 Q 30,30 30,60 Q 30,90 50,100" stroke="#FFFFFF" stroke-width="6" fill="none" stroke-linecap="round" stroke-dasharray="8,8"/>
|
||||||
|
|
||||||
|
<!-- Highlight -->
|
||||||
|
<path d="M 48,15 Q 32,32 32,60 Q 32,88 48,98" stroke="#FFFFFF" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 546 B |
25
web/assets/gift.svg
Normal file
25
web/assets/gift.svg
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<svg viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Box -->
|
||||||
|
<rect x="15" y="30" width="70" height="70" fill="#E74C3C" stroke="#C0392B" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Box lid/3D effect -->
|
||||||
|
<polygon points="15,30 25,20 85,20 75,30" fill="#C0392B"/>
|
||||||
|
<polygon points="75,30 85,20 85,90 75,100" fill="#A93226"/>
|
||||||
|
|
||||||
|
<!-- Ribbon vertical -->
|
||||||
|
<rect x="42" y="20" width="16" height="85" fill="#FFD700" stroke="#DAA520" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Ribbon horizontal -->
|
||||||
|
<rect x="10" y="57" width="80" height="16" fill="#FFD700" stroke="#DAA520" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Bow on top -->
|
||||||
|
<ellipse cx="35" cy="18" rx="10" ry="8" fill="#FFD700" stroke="#DAA520" stroke-width="1"/>
|
||||||
|
<ellipse cx="65" cy="18" rx="10" ry="8" fill="#FFD700" stroke="#DAA520" stroke-width="1"/>
|
||||||
|
<circle cx="50" cy="20" r="5" fill="#DAA520"/>
|
||||||
|
|
||||||
|
<!-- Pattern on box -->
|
||||||
|
<circle cx="30" cy="50" r="3" fill="#FFFFFF" opacity="0.5"/>
|
||||||
|
<circle cx="70" cy="60" r="3" fill="#FFFFFF" opacity="0.5"/>
|
||||||
|
<circle cx="50" cy="75" r="3" fill="#FFFFFF" opacity="0.5"/>
|
||||||
|
<circle cx="35" cy="80" r="3" fill="#FFFFFF" opacity="0.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
39
web/assets/reindeer.svg
Normal file
39
web/assets/reindeer.svg
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<svg viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Antlers -->
|
||||||
|
<path d="M 35,25 Q 25,10 20,5" stroke="#8B4513" stroke-width="3" fill="none"/>
|
||||||
|
<path d="M 65,25 Q 75,10 80,5" stroke="#8B4513" stroke-width="3" fill="none"/>
|
||||||
|
<path d="M 33,22 Q 22,15 15,8" stroke="#8B4513" stroke-width="2" fill="none"/>
|
||||||
|
<path d="M 67,22 Q 78,15 85,8" stroke="#8B4513" stroke-width="2" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Head -->
|
||||||
|
<circle cx="50" cy="35" r="15" fill="#8B4513"/>
|
||||||
|
|
||||||
|
<!-- Ears -->
|
||||||
|
<ellipse cx="38" cy="22" rx="5" ry="8" fill="#8B4513"/>
|
||||||
|
<ellipse cx="62" cy="22" rx="5" ry="8" fill="#8B4513"/>
|
||||||
|
|
||||||
|
<!-- Eyes -->
|
||||||
|
<circle cx="45" cy="32" r="2" fill="#000000"/>
|
||||||
|
<circle cx="55" cy="32" r="2" fill="#000000"/>
|
||||||
|
|
||||||
|
<!-- Nose (red) -->
|
||||||
|
<circle cx="50" cy="40" r="4" fill="#E74C3C"/>
|
||||||
|
|
||||||
|
<!-- Mouth -->
|
||||||
|
<path d="M 48,45 Q 50,47 52,45" stroke="#000000" stroke-width="1" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Neck -->
|
||||||
|
<rect x="43" y="48" width="14" height="8" fill="#8B4513"/>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<ellipse cx="50" cy="70" rx="20" ry="25" fill="#8B4513"/>
|
||||||
|
|
||||||
|
<!-- Legs -->
|
||||||
|
<rect x="35" y="90" width="6" height="25" fill="#8B4513"/>
|
||||||
|
<rect x="45" y="90" width="6" height="25" fill="#8B4513"/>
|
||||||
|
<rect x="55" y="90" width="6" height="25" fill="#8B4513"/>
|
||||||
|
<rect x="65" y="90" width="6" height="25" fill="#8B4513"/>
|
||||||
|
|
||||||
|
<!-- Tail -->
|
||||||
|
<circle cx="68" cy="65" r="5" fill="#FFFFFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
37
web/assets/santa.svg
Normal file
37
web/assets/santa.svg
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<svg viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Santa hat -->
|
||||||
|
<polygon points="20,20 80,20 75,35 25,35" fill="#E74C3C"/>
|
||||||
|
<circle cx="50" cy="18" r="8" fill="#E74C3C"/>
|
||||||
|
<circle cx="77" cy="28" r="6" fill="#FFFFFF"/>
|
||||||
|
|
||||||
|
<!-- Face -->
|
||||||
|
<circle cx="50" cy="50" r="18" fill="#F5DEB3"/>
|
||||||
|
|
||||||
|
<!-- Eyes -->
|
||||||
|
<circle cx="42" cy="45" r="2.5" fill="#000000"/>
|
||||||
|
<circle cx="58" cy="45" r="2.5" fill="#000000"/>
|
||||||
|
|
||||||
|
<!-- Nose -->
|
||||||
|
<circle cx="50" cy="52" r="2" fill="#E74C3C"/>
|
||||||
|
|
||||||
|
<!-- Beard -->
|
||||||
|
<path d="M 35,58 Q 35,65 50,68 Q 65,65 65,58" fill="#FFFFFF"/>
|
||||||
|
|
||||||
|
<!-- Mouth -->
|
||||||
|
<path d="M 42,60 Q 50,63 58,60" stroke="#000000" stroke-width="1" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<rect x="35" y="68" width="30" height="25" rx="5" fill="#E74C3C"/>
|
||||||
|
|
||||||
|
<!-- Belt -->
|
||||||
|
<rect x="32" y="85" width="36" height="4" fill="#000000"/>
|
||||||
|
<circle cx="68" cy="87" r="2.5" fill="#FFD700"/>
|
||||||
|
|
||||||
|
<!-- Arms -->
|
||||||
|
<rect x="15" y="75" width="20" height="6" rx="3" fill="#F5DEB3"/>
|
||||||
|
<rect x="65" y="75" width="20" height="6" rx="3" fill="#F5DEB3"/>
|
||||||
|
|
||||||
|
<!-- Legs -->
|
||||||
|
<rect x="40" y="93" width="8" height="20" fill="#000000"/>
|
||||||
|
<rect x="52" y="93" width="8" height="20" fill="#000000"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
17
web/assets/tree.svg
Normal file
17
web/assets/tree.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<svg viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Tree trunk -->
|
||||||
|
<rect x="40" y="80" width="20" height="25" fill="#8B4513"/>
|
||||||
|
|
||||||
|
<!-- Tree layers -->
|
||||||
|
<polygon points="50,10 20,50 30,50 10,80 40,80 5,110 50,110 95,110 60,80 90,80 70,50 80,50" fill="#228B22"/>
|
||||||
|
|
||||||
|
<!-- Tree highlights -->
|
||||||
|
<circle cx="50" cy="35" r="4" fill="#FFD700" opacity="0.7"/>
|
||||||
|
<circle cx="35" cy="55" r="3" fill="#FFD700" opacity="0.7"/>
|
||||||
|
<circle cx="65" cy="60" r="3" fill="#FFD700" opacity="0.7"/>
|
||||||
|
<circle cx="45" cy="80" r="3" fill="#FFD700" opacity="0.7"/>
|
||||||
|
<circle cx="55" cy="90" r="3" fill="#FFD700" opacity="0.7"/>
|
||||||
|
|
||||||
|
<!-- Star on top -->
|
||||||
|
<polygon points="50,5 55,15 65,15 57,20 60,30 50,25 40,30 43,20 35,15 45,15" fill="#FFD700"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 758 B |
335
web/index.html
335
web/index.html
@@ -132,6 +132,226 @@
|
|||||||
grid-template-columns: 1fr; /* Stack columns on smaller screens */
|
grid-template-columns: 1fr; /* Stack columns on smaller screens */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Christmas toggle switch */
|
||||||
|
.christmas-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-checkbox {
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
background-color: #555;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-checkbox:checked {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-checkbox::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: white;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
transition: left 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-checkbox:checked::before {
|
||||||
|
left: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Santa hat styles */
|
||||||
|
.santa-hat {
|
||||||
|
position: absolute;
|
||||||
|
width: 60px;
|
||||||
|
height: 50px;
|
||||||
|
top: -20px;
|
||||||
|
transform: rotate(-20deg);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.santa-hat::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 70%;
|
||||||
|
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||||
|
clip-path: polygon(0 0, 100% 0, 90% 100%, 10% 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.santa-hat::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
bottom: -5px;
|
||||||
|
right: -8px;
|
||||||
|
box-shadow: -15px 5px 0 -5px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Jingle bell styles */
|
||||||
|
.jingle-bell {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 12px;
|
||||||
|
height: 14px;
|
||||||
|
margin: 0 2px;
|
||||||
|
animation: jingle 0.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jingle-bell::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f1c40f;
|
||||||
|
border-radius: 50% 50% 50% 0;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jingle-bell::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 3px;
|
||||||
|
height: 6px;
|
||||||
|
background: #d4a500;
|
||||||
|
top: -6px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes jingle {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(5deg); }
|
||||||
|
75% { transform: rotate(-5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Snow animation */
|
||||||
|
.snowflake {
|
||||||
|
position: fixed;
|
||||||
|
top: -10px;
|
||||||
|
color: white;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 5px rgba(255,255,255,0.8);
|
||||||
|
z-index: 1;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: snowfall linear infinite;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes snowfall {
|
||||||
|
to {
|
||||||
|
transform: translateY(100vh) translateX(100px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.christmas-active .snowflake {
|
||||||
|
animation: snowfall linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Festive header when active */
|
||||||
|
body.christmas-active header {
|
||||||
|
background: linear-gradient(90deg, #27ae60 0%, #e74c3c 50%, #27ae60 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: festive-pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes festive-pulse {
|
||||||
|
0%, 100% { background-position: 0% 0%; }
|
||||||
|
50% { background-position: 100% 0%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Jingle bells in header when active */
|
||||||
|
body.christmas-active h1::before {
|
||||||
|
content: '🔔 ';
|
||||||
|
animation: jingle 0.4s ease-in-out infinite;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 30px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.christmas-active h1::after {
|
||||||
|
content: ' 🔔';
|
||||||
|
animation: jingle 0.4s ease-in-out infinite;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 30px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner decorations */
|
||||||
|
.corner-decoration {
|
||||||
|
position: fixed;
|
||||||
|
font-size: 80px;
|
||||||
|
z-index: 5;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.9;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-decoration img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bottom decorations */
|
||||||
|
.bottom-decoration {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
z-index: 5;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-decoration img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-decoration.bottom-left {
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
animation: sway 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-decoration.bottom-right {
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
animation: sway 3s ease-in-out infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sway {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
50% { transform: rotate(-5deg); }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -191,6 +411,118 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Christmas mode toggle functionality
|
||||||
|
function initChristmasMode() {
|
||||||
|
// Check URL parameter first for override
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const christmasParam = urlParams.get('christmas');
|
||||||
|
|
||||||
|
let shouldEnable = false;
|
||||||
|
|
||||||
|
if (christmasParam === 'on') {
|
||||||
|
shouldEnable = true;
|
||||||
|
} else if (christmasParam === 'off') {
|
||||||
|
shouldEnable = false;
|
||||||
|
} else {
|
||||||
|
// Auto-enable for December
|
||||||
|
const now = new Date();
|
||||||
|
shouldEnable = now.getMonth() === 11; // December is month 11 (0-indexed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldEnable) {
|
||||||
|
enableChristmasMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableChristmasMode() {
|
||||||
|
document.body.classList.add('christmas-active');
|
||||||
|
|
||||||
|
// Create falling snowflakes
|
||||||
|
function createSnowflake() {
|
||||||
|
const snowflake = document.createElement('div');
|
||||||
|
snowflake.classList.add('snowflake');
|
||||||
|
snowflake.textContent = '❄';
|
||||||
|
snowflake.style.left = Math.random() * window.innerWidth + 'px';
|
||||||
|
snowflake.style.animationDuration = (Math.random() * 5 + 8) + 's';
|
||||||
|
snowflake.style.animationDelay = Math.random() * 2 + 's';
|
||||||
|
|
||||||
|
document.body.appendChild(snowflake);
|
||||||
|
|
||||||
|
setTimeout(() => snowflake.remove(), 13000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create snowflakes periodically
|
||||||
|
const snowInterval = setInterval(() => {
|
||||||
|
if (!document.body.classList.contains('christmas-active')) {
|
||||||
|
clearInterval(snowInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createSnowflake();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
// Add corner decorations
|
||||||
|
const leftCorner = document.createElement('div');
|
||||||
|
leftCorner.classList.add('corner-decoration', 'bottom-left');
|
||||||
|
const treeImg = document.createElement('img');
|
||||||
|
treeImg.src = 'assets/tree.svg';
|
||||||
|
treeImg.alt = 'Christmas Tree';
|
||||||
|
leftCorner.appendChild(treeImg);
|
||||||
|
leftCorner.id = 'corner-left';
|
||||||
|
document.body.appendChild(leftCorner);
|
||||||
|
|
||||||
|
const rightCorner = document.createElement('div');
|
||||||
|
rightCorner.classList.add('corner-decoration', 'bottom-right');
|
||||||
|
const santaImg = document.createElement('img');
|
||||||
|
santaImg.src = 'assets/santa.svg';
|
||||||
|
santaImg.alt = 'Santa';
|
||||||
|
rightCorner.appendChild(santaImg);
|
||||||
|
rightCorner.id = 'corner-right';
|
||||||
|
document.body.appendChild(rightCorner);
|
||||||
|
|
||||||
|
// Add bottom decorations in a row
|
||||||
|
const bottomDecorations = [
|
||||||
|
{ src: 'assets/reindeer.svg', alt: 'Reindeer' },
|
||||||
|
{ src: 'assets/bell.svg', alt: 'Bell' },
|
||||||
|
{ src: 'assets/gift.svg', alt: 'Gift' },
|
||||||
|
{ src: 'assets/candycane.svg', alt: 'Candy Cane' },
|
||||||
|
{ src: 'assets/bell.svg', alt: 'Bell' },
|
||||||
|
{ src: 'assets/gift.svg', alt: 'Gift' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const screenWidth = window.innerWidth;
|
||||||
|
const totalDecorations = bottomDecorations.length;
|
||||||
|
const spacing = screenWidth / (totalDecorations + 1);
|
||||||
|
|
||||||
|
bottomDecorations.forEach((deco, index) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.classList.add('bottom-decoration');
|
||||||
|
div.style.left = (spacing * (index + 1) - 40) + 'px'; // 40 is half the width
|
||||||
|
div.style.animation = `sway ${3 + index * 0.5}s ease-in-out infinite`;
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = deco.src;
|
||||||
|
img.alt = deco.alt;
|
||||||
|
div.appendChild(img);
|
||||||
|
div.id = `bottom-deco-${index}`;
|
||||||
|
|
||||||
|
document.body.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableChristmasMode() {
|
||||||
|
document.body.classList.remove('christmas-active');
|
||||||
|
|
||||||
|
// Remove corner decorations
|
||||||
|
document.getElementById('corner-left')?.remove();
|
||||||
|
document.getElementById('corner-right')?.remove();
|
||||||
|
|
||||||
|
// Remove bottom decorations
|
||||||
|
document.querySelectorAll('[id^="bottom-deco-"]').forEach(deco => deco.remove());
|
||||||
|
|
||||||
|
// Remove snowflakes
|
||||||
|
document.querySelectorAll('.snowflake').forEach(flake => flake.remove());
|
||||||
|
}
|
||||||
|
|
||||||
let wsConnection = null;
|
let wsConnection = null;
|
||||||
|
|
||||||
// ICAO code to airport name cache
|
// ICAO code to airport name cache
|
||||||
@@ -517,6 +849,9 @@
|
|||||||
|
|
||||||
// Load data on page load
|
// Load data on page load
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
|
// Initialize Christmas mode
|
||||||
|
initChristmasMode();
|
||||||
|
|
||||||
loadArrivals();
|
loadArrivals();
|
||||||
loadDepartures();
|
loadDepartures();
|
||||||
|
|
||||||
|
|||||||
336
web/reports.html
336
web/reports.html
@@ -378,18 +378,26 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Filters Section -->
|
<!-- Filters Section -->
|
||||||
<div class="filters-section">
|
<div class="filters-section">
|
||||||
<div class="filters-grid">
|
<div style="display: flex; gap: 0.5rem; align-items: flex-end; flex-wrap: wrap;">
|
||||||
<div class="filter-group">
|
<!-- Quick Filter Buttons -->
|
||||||
<label for="date-from">Date From:</label>
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
<input type="date" id="date-from">
|
<button class="btn btn-primary" id="filter-today" onclick="setDateRangeToday()" style="font-size: 0.9rem; padding: 0.5rem 1rem; white-space: nowrap;">📅 Today</button>
|
||||||
|
<button class="btn btn-secondary" id="filter-week" onclick="setDateRangeThisWeek()" style="font-size: 0.9rem; padding: 0.5rem 1rem; white-space: nowrap;">📆 This Week</button>
|
||||||
|
<button class="btn btn-secondary" id="filter-month" onclick="setDateRangeThisMonth()" style="font-size: 0.9rem; padding: 0.5rem 1rem; white-space: nowrap;">📊 This Month</button>
|
||||||
|
<button class="btn btn-secondary" id="filter-custom" onclick="toggleCustomRange()" style="font-size: 0.9rem; padding: 0.5rem 1rem; white-space: nowrap;">📋 Custom Range</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
|
||||||
<label for="date-to">Date To:</label>
|
<!-- Custom Date Range (hidden by default) -->
|
||||||
<input type="date" id="date-to">
|
<div id="custom-range-container" style="display: none; display: flex; gap: 0.5rem; align-items: center;">
|
||||||
|
<input type="date" id="date-from" style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
<span style="font-weight: 600; color: #666;">to</span>
|
||||||
|
<input type="date" id="date-to" style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
|
||||||
<label for="status-filter">Status:</label>
|
<!-- Status Filter -->
|
||||||
<select id="status-filter">
|
<div style="display: flex; flex-direction: column; gap: 0.3rem;">
|
||||||
|
<label for="status-filter" style="font-weight: 600; font-size: 0.85rem; color: #555;">Status:</label>
|
||||||
|
<select id="status-filter" style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; font-size: 0.9rem;">
|
||||||
<option value="">All Statuses</option>
|
<option value="">All Statuses</option>
|
||||||
<option value="NEW">New</option>
|
<option value="NEW">New</option>
|
||||||
<option value="CONFIRMED">Confirmed</option>
|
<option value="CONFIRMED">Confirmed</option>
|
||||||
@@ -399,15 +407,19 @@
|
|||||||
<option value="DELETED">Deleted</option>
|
<option value="DELETED">Deleted</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
|
||||||
<label for="search-input">Search:</label>
|
<!-- Search Input -->
|
||||||
<input type="text" id="search-input" placeholder="Aircraft reg, callsign, captain, or airport...">
|
<div style="flex: 1; min-width: 200px; display: flex; flex-direction: column; gap: 0.3rem;">
|
||||||
|
<label for="search-input" style="font-weight: 600; font-size: 0.85rem; color: #555;">Search:</label>
|
||||||
|
<input type="text" id="search-input" placeholder="Aircraft reg, callsign, captain, or airport..." style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; font-size: 0.9rem;">
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-actions">
|
|
||||||
<button class="btn btn-primary" onclick="loadReports()">
|
<!-- Action Buttons -->
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<button class="btn btn-primary" onclick="loadReports()" style="font-size: 0.9rem; padding: 0.5rem 1rem; white-space: nowrap;">
|
||||||
🔍 Search
|
🔍 Search
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" onclick="clearFilters()">
|
<button class="btn btn-secondary" onclick="clearFilters()" style="font-size: 0.9rem; padding: 0.5rem 1rem; white-space: nowrap;">
|
||||||
🗑️ Clear
|
🗑️ Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -417,67 +429,69 @@
|
|||||||
<!-- Summary Box -->
|
<!-- Summary Box -->
|
||||||
<div class="summary-box">
|
<div class="summary-box">
|
||||||
<div class="summary-title">📊 Movements Summary</div>
|
<div class="summary-title">📊 Movements Summary</div>
|
||||||
<div class="summary-grid">
|
<div style="display: grid; grid-template-columns: 1fr auto; gap: 2rem; align-items: center;">
|
||||||
<!-- PPR Section -->
|
<div class="summary-grid">
|
||||||
<div style="grid-column: 1/-1; padding-bottom: 1rem; border-bottom: 2px solid rgba(255,255,255,0.3);">
|
<!-- PPR Section -->
|
||||||
<div style="font-size: 0.95rem; font-weight: 600; margin-bottom: 0.6rem;">PPR Movements</div>
|
<div style="grid-column: 1/-1; padding-bottom: 0.8rem; border-bottom: 2px solid rgba(255,255,255,0.3);">
|
||||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
|
<div style="font-size: 0.85rem; font-weight: 600; margin-bottom: 0.4rem;">PPR Movements</div>
|
||||||
<div class="summary-item" style="padding: 0.6rem;">
|
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.8rem;">
|
||||||
<div class="summary-item-label" style="font-size: 0.75rem; margin-bottom: 0.2rem;">Arrivals (Landings)</div>
|
<div class="summary-item" style="padding: 0.4rem;">
|
||||||
<div class="summary-item-value" style="font-size: 1.3rem;" id="ppr-arrivals">0</div>
|
<div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Arrivals</div>
|
||||||
|
<div class="summary-item-value" style="font-size: 1.1rem;" id="ppr-arrivals">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item" style="padding: 0.4rem;">
|
||||||
|
<div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Departures</div>
|
||||||
|
<div class="summary-item-value" style="font-size: 1.1rem;" id="ppr-departures">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item" style="border-left-color: #ffd700; background: rgba(255,215,0,0.1); padding: 0.4rem;">
|
||||||
|
<div class="summary-item-label" style="font-weight: 600; font-size: 0.7rem; margin-bottom: 0.1rem;">Total</div>
|
||||||
|
<div class="summary-item-value" style="font-size: 1.1rem;" id="ppr-total">0</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item" style="padding: 0.6rem;">
|
</div>
|
||||||
<div class="summary-item-label" style="font-size: 0.75rem; margin-bottom: 0.2rem;">Departures (Takeoffs)</div>
|
|
||||||
<div class="summary-item-value" style="font-size: 1.3rem;" id="ppr-departures">0</div>
|
<!-- Non-PPR Section -->
|
||||||
</div>
|
<div style="grid-column: 1/-1; padding-top: 0.8rem;">
|
||||||
<div class="summary-item" style="border-left-color: #ffd700; background: rgba(255,215,0,0.1); padding: 0.6rem;">
|
<div style="font-size: 0.85rem; font-weight: 600; margin-bottom: 0.4rem;">Non-PPR Movements</div>
|
||||||
<div class="summary-item-label" style="font-weight: 600; font-size: 0.75rem; margin-bottom: 0.2rem;">PPR Total</div>
|
<div style="display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.8rem;">
|
||||||
<div class="summary-item-value" style="font-size: 1.3rem;" id="ppr-total">0</div>
|
<div class="summary-item" style="padding: 0.4rem; cursor: pointer;" onclick="filterOtherFlights('LOCAL')">
|
||||||
|
<div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Local</div>
|
||||||
|
<div class="summary-item-value" style="font-size: 1.1rem;" id="local-flights-movements">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item" style="padding: 0.4rem; cursor: pointer;" onclick="filterOtherFlights('CIRCUIT')">
|
||||||
|
<div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Circuits</div>
|
||||||
|
<div class="summary-item-value" style="font-size: 1.1rem;" id="circuits-movements">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item" style="padding: 0.4rem; cursor: pointer;" onclick="filterOtherFlights('ARRIVAL')">
|
||||||
|
<div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Arrivals</div>
|
||||||
|
<div class="summary-item-value" style="font-size: 1.1rem;" id="non-ppr-arrivals">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item" style="padding: 0.4rem; cursor: pointer;" onclick="filterOtherFlights('DEPARTURE')">
|
||||||
|
<div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Departures</div>
|
||||||
|
<div class="summary-item-value" style="font-size: 1.1rem;" id="non-ppr-departures">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item" style="padding: 0.4rem; cursor: pointer;" onclick="filterOtherFlights('OVERFLIGHT')">
|
||||||
|
<div class="summary-item-label" style="font-size: 0.7rem; margin-bottom: 0.1rem;">Overflights</div>
|
||||||
|
<div class="summary-item-value" style="font-size: 1.1rem;" id="overflights-count">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item" style="border-left-color: #ffd700; background: rgba(255,215,0,0.1); padding: 0.4rem;">
|
||||||
|
<div class="summary-item-label" style="font-weight: 600; font-size: 0.7rem; margin-bottom: 0.1rem;">Total</div>
|
||||||
|
<div class="summary-item-value" style="font-size: 1.1rem;" id="non-ppr-total">0</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Non-PPR Section -->
|
<!-- Grand Total - positioned on the right -->
|
||||||
<div style="grid-column: 1/-1; padding-bottom: 1rem; border-bottom: 2px solid rgba(255,255,255,0.3);">
|
<div style="text-align: center; padding: 1rem; background: rgba(255,215,0,0.05); border-radius: 8px; border-left: 4px solid #ffd700; min-width: 120px;">
|
||||||
<div style="font-size: 0.95rem; font-weight: 600; margin-bottom: 0.6rem;">Non-PPR Movements</div>
|
<div style="font-size: 0.75rem; opacity: 0.85; margin-bottom: 0.2rem;">GRAND TOTAL</div>
|
||||||
<div style="display: grid; grid-template-columns: repeat(6, 1fr); gap: 1rem;">
|
<div style="font-size: 2.2rem; font-weight: 700;" id="grand-total-movements">0</div>
|
||||||
<div class="summary-item" style="padding: 0.6rem;">
|
|
||||||
<div class="summary-item-label" style="font-size: 0.75rem; margin-bottom: 0.2rem;">Local Flights</div>
|
|
||||||
<div class="summary-item-value" style="font-size: 1.3rem;" id="local-flights-movements">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item" style="padding: 0.6rem;">
|
|
||||||
<div class="summary-item-label" style="font-size: 0.75rem; margin-bottom: 0.2rem;">Circuits</div>
|
|
||||||
<div class="summary-item-value" style="font-size: 1.3rem;" id="circuits-movements">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item" style="padding: 0.6rem;">
|
|
||||||
<div class="summary-item-label" style="font-size: 0.75rem; margin-bottom: 0.2rem;">Arrivals</div>
|
|
||||||
<div class="summary-item-value" style="font-size: 1.3rem;" id="non-ppr-arrivals">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item" style="padding: 0.6rem;">
|
|
||||||
<div class="summary-item-label" style="font-size: 0.75rem; margin-bottom: 0.2rem;">Departures</div>
|
|
||||||
<div class="summary-item-value" style="font-size: 1.3rem;" id="non-ppr-departures">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item" style="padding: 0.6rem;">
|
|
||||||
<div class="summary-item-label" style="font-size: 0.75rem; margin-bottom: 0.2rem;">Overflights</div>
|
|
||||||
<div class="summary-item-value" style="font-size: 1.3rem;" id="overflights-count">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item" style="border-left-color: #ffd700; background: rgba(255,215,0,0.1); padding: 0.6rem;">
|
|
||||||
<div class="summary-item-label" style="font-weight: 600; font-size: 0.75rem; margin-bottom: 0.2rem;">Non-PPR Total</div>
|
|
||||||
<div class="summary-item-value" style="font-size: 1.3rem;" id="non-ppr-total">0</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Grand Total -->
|
|
||||||
<div style="grid-column: 1/-1; text-align: center;">
|
|
||||||
<div style="font-size: 0.85rem; opacity: 0.9; margin-bottom: 0.3rem;">Grand Total Movements</div>
|
|
||||||
<div style="font-size: 2rem; font-weight: 700;" id="grand-total-movements">0</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reports Table -->
|
<!-- Reports Table -->
|
||||||
<div class="reports-table">
|
<div class="reports-table" id="ppr-reports-section">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<div>
|
<div>
|
||||||
<strong>PPR Records</strong>
|
<strong>PPR Records</strong>
|
||||||
@@ -590,6 +604,7 @@
|
|||||||
let accessToken = null;
|
let accessToken = null;
|
||||||
let currentPPRs = []; // Store current results for export
|
let currentPPRs = []; // Store current results for export
|
||||||
let currentOtherFlights = []; // Store other flights for export
|
let currentOtherFlights = []; // Store other flights for export
|
||||||
|
let otherFlightsFilterType = null; // Track which non-PPR flight type is selected for filtering
|
||||||
|
|
||||||
// Load UI configuration from API
|
// Load UI configuration from API
|
||||||
async function loadUIConfig() {
|
async function loadUIConfig() {
|
||||||
@@ -639,14 +654,105 @@
|
|||||||
await loadReports();
|
await loadReports();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default date range to current month
|
// Set default date range to today
|
||||||
function setupDefaultDateRange() {
|
function setupDefaultDateRange() {
|
||||||
|
setDateRangeToday();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle custom date range picker
|
||||||
|
function toggleCustomRange() {
|
||||||
|
const container = document.getElementById('custom-range-container');
|
||||||
|
const customBtn = document.getElementById('filter-custom');
|
||||||
|
|
||||||
|
const isVisible = container.style.display !== 'none';
|
||||||
|
container.style.display = isVisible ? 'none' : 'flex';
|
||||||
|
|
||||||
|
// Update button style
|
||||||
|
if (isVisible) {
|
||||||
|
customBtn.classList.remove('btn-primary');
|
||||||
|
customBtn.classList.add('btn-secondary');
|
||||||
|
} else {
|
||||||
|
customBtn.classList.remove('btn-secondary');
|
||||||
|
customBtn.classList.add('btn-primary');
|
||||||
|
// Focus on the first date input when opening
|
||||||
|
document.getElementById('date-from').focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set date range to today
|
||||||
|
function setDateRangeToday() {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
document.getElementById('date-from').value = today;
|
||||||
|
document.getElementById('date-to').value = today;
|
||||||
|
|
||||||
|
// Hide custom range picker if it's open
|
||||||
|
document.getElementById('custom-range-container').style.display = 'none';
|
||||||
|
|
||||||
|
updateFilterButtonStyles('today');
|
||||||
|
loadReports();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set date range to this week (Monday to Sunday)
|
||||||
|
function setDateRangeThisWeek() {
|
||||||
|
const now = new Date();
|
||||||
|
const dayOfWeek = now.getDay();
|
||||||
|
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust when day is Sunday
|
||||||
|
const monday = new Date(now.setDate(diff));
|
||||||
|
const sunday = new Date(now.setDate(diff + 6));
|
||||||
|
|
||||||
|
document.getElementById('date-from').value = monday.toISOString().split('T')[0];
|
||||||
|
document.getElementById('date-to').value = sunday.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Hide custom range picker if it's open
|
||||||
|
document.getElementById('custom-range-container').style.display = 'none';
|
||||||
|
|
||||||
|
updateFilterButtonStyles('week');
|
||||||
|
loadReports();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set date range to this month
|
||||||
|
function setDateRangeThisMonth() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||||
|
|
||||||
document.getElementById('date-from').value = firstDay.toISOString().split('T')[0];
|
document.getElementById('date-from').value = firstDay.toISOString().split('T')[0];
|
||||||
document.getElementById('date-to').value = lastDay.toISOString().split('T')[0];
|
document.getElementById('date-to').value = lastDay.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Hide custom range picker if it's open
|
||||||
|
document.getElementById('custom-range-container').style.display = 'none';
|
||||||
|
|
||||||
|
updateFilterButtonStyles('month');
|
||||||
|
loadReports();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update button styles to show which filter is active
|
||||||
|
function updateFilterButtonStyles(activeFilter) {
|
||||||
|
const todayBtn = document.getElementById('filter-today');
|
||||||
|
const weekBtn = document.getElementById('filter-week');
|
||||||
|
const monthBtn = document.getElementById('filter-month');
|
||||||
|
|
||||||
|
// Reset all buttons
|
||||||
|
[todayBtn, weekBtn, monthBtn].forEach(btn => {
|
||||||
|
btn.classList.remove('btn-primary');
|
||||||
|
btn.classList.add('btn-secondary');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlight active button
|
||||||
|
switch(activeFilter) {
|
||||||
|
case 'today':
|
||||||
|
todayBtn.classList.remove('btn-secondary');
|
||||||
|
todayBtn.classList.add('btn-primary');
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
weekBtn.classList.remove('btn-secondary');
|
||||||
|
weekBtn.classList.add('btn-primary');
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
monthBtn.classList.remove('btn-secondary');
|
||||||
|
monthBtn.classList.add('btn-primary');
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication management
|
// Authentication management
|
||||||
@@ -985,20 +1091,103 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter other flights by type
|
||||||
|
function filterOtherFlights(flightType) {
|
||||||
|
// Toggle filter if clicking the same type
|
||||||
|
if (otherFlightsFilterType === flightType) {
|
||||||
|
otherFlightsFilterType = null;
|
||||||
|
} else {
|
||||||
|
otherFlightsFilterType = flightType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide PPR section based on filter
|
||||||
|
const pprSection = document.getElementById('ppr-reports-section');
|
||||||
|
if (pprSection) {
|
||||||
|
pprSection.style.display = otherFlightsFilterType ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update visual indication of active filter
|
||||||
|
updateFilterIndicators();
|
||||||
|
|
||||||
|
// Re-display flights with new filter
|
||||||
|
displayOtherFlights(currentOtherFlights);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update visual indicators for active filter
|
||||||
|
function updateFilterIndicators() {
|
||||||
|
// Select all clickable non-PPR summary items (those with onclick attribute)
|
||||||
|
const summaryItems = document.querySelectorAll('.summary-item[onclick*="filterOtherFlights"]');
|
||||||
|
summaryItems.forEach(item => {
|
||||||
|
item.style.opacity = '1';
|
||||||
|
item.style.borderLeftColor = '';
|
||||||
|
item.style.borderLeftWidth = '0';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (otherFlightsFilterType) {
|
||||||
|
// Get the ID of the selected filter's summary item
|
||||||
|
let selectedId = '';
|
||||||
|
switch(otherFlightsFilterType) {
|
||||||
|
case 'LOCAL':
|
||||||
|
selectedId = 'local-flights-movements';
|
||||||
|
break;
|
||||||
|
case 'CIRCUIT':
|
||||||
|
selectedId = 'circuits-movements';
|
||||||
|
break;
|
||||||
|
case 'ARRIVAL':
|
||||||
|
selectedId = 'non-ppr-arrivals';
|
||||||
|
break;
|
||||||
|
case 'DEPARTURE':
|
||||||
|
selectedId = 'non-ppr-departures';
|
||||||
|
break;
|
||||||
|
case 'OVERFLIGHT':
|
||||||
|
selectedId = 'overflights-count';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and highlight the selected item
|
||||||
|
if (selectedId) {
|
||||||
|
const selectedElement = document.getElementById(selectedId);
|
||||||
|
if (selectedElement) {
|
||||||
|
const summaryItem = selectedElement.closest('.summary-item');
|
||||||
|
if (summaryItem) {
|
||||||
|
summaryItem.style.borderLeftColor = '#4CAF50';
|
||||||
|
summaryItem.style.borderLeftWidth = '4px';
|
||||||
|
summaryItem.style.opacity = '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dim other items that are clickable (non-PPR items)
|
||||||
|
const allSummaryItems = document.querySelectorAll('.summary-item[onclick]');
|
||||||
|
allSummaryItems.forEach(item => {
|
||||||
|
if (item.querySelector('#' + selectedId) === null) {
|
||||||
|
item.style.opacity = '0.5';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Display other flights in table
|
// Display other flights in table
|
||||||
function displayOtherFlights(flights) {
|
function displayOtherFlights(flights) {
|
||||||
const tbody = document.getElementById('other-flights-table-body');
|
const tbody = document.getElementById('other-flights-table-body');
|
||||||
const tableInfo = document.getElementById('other-flights-info');
|
const tableInfo = document.getElementById('other-flights-info');
|
||||||
|
|
||||||
tableInfo.textContent = `${flights.length} flights found`;
|
// Apply filter if one is selected
|
||||||
|
let filteredFlights = flights;
|
||||||
|
if (otherFlightsFilterType) {
|
||||||
|
filteredFlights = flights.filter(flight => flight.flightType === otherFlightsFilterType);
|
||||||
|
}
|
||||||
|
|
||||||
if (flights.length === 0) {
|
tableInfo.textContent = `${filteredFlights.length} flights found` + (otherFlightsFilterType ? ` (filtered by ${otherFlightsFilterType})` : '');
|
||||||
|
|
||||||
|
if (filteredFlights.length === 0) {
|
||||||
document.getElementById('other-flights-no-data').style.display = 'block';
|
document.getElementById('other-flights-no-data').style.display = 'block';
|
||||||
|
document.getElementById('other-flights-table-content').style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by time field (ascending)
|
// Sort by time field (ascending)
|
||||||
flights.sort((a, b) => {
|
filteredFlights.sort((a, b) => {
|
||||||
const aTime = a.timeField;
|
const aTime = a.timeField;
|
||||||
const bTime = b.timeField;
|
const bTime = b.timeField;
|
||||||
if (!aTime) return 1;
|
if (!aTime) return 1;
|
||||||
@@ -1008,8 +1197,9 @@
|
|||||||
|
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
document.getElementById('other-flights-table-content').style.display = 'block';
|
document.getElementById('other-flights-table-content').style.display = 'block';
|
||||||
|
document.getElementById('other-flights-no-data').style.display = 'none';
|
||||||
|
|
||||||
for (const flight of flights) {
|
for (const flight of filteredFlights) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
const typeLabel = flight.flightType;
|
const typeLabel = flight.flightType;
|
||||||
|
|||||||
Reference in New Issue
Block a user