Compare commits

..

2 Commits

4 changed files with 638 additions and 2 deletions
+455
View File
@@ -0,0 +1,455 @@
<?php
$enablePrototypeComments = true;
include '../../header-telemetria.php';
?>
<link rel="stylesheet" href="<?= $basePath ?>/assets/vendor/libs/leaflet/leaflet.css" />
<style>
#installationsMap {
height: calc(100vh - 220px);
min-height: 480px;
width: 100%;
z-index: 1;
}
.map-sidebar-list {
max-height: calc(100vh - 420px);
min-height: 220px;
overflow-y: auto;
}
.map-marker-pin {
width: 32px;
height: 32px;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid #fff;
box-shadow: 0 2px 6px rgba(67, 89, 113, 0.35);
display: flex;
align-items: center;
justify-content: center;
background-color: #696cff;
}
.map-marker-pin i {
transform: rotate(45deg);
color: #fff;
font-size: 14px;
line-height: 1;
}
.map-marker-pin.is-alert {
background-color: #ff3e1d;
}
.map-list-item {
cursor: pointer;
}
.map-list-item:hover,
.map-list-item.is-focused {
background-color: rgba(105, 108, 255, 0.08);
}
.map-list-item.is-hidden-on-map {
opacity: 0.55;
}
</style>
<div class="container-fluid flex-grow-1 container-p-y">
<h4 class="fw-bold py-3 mb-4">
Telemetria <span class="text-muted fw-light">/ Mapa instalacji</span>
</h4>
<div class="row g-4">
<div class="col-lg-8 col-xl-9">
<div class="card shadow-sm border-0 overflow-hidden">
<div id="installationsMap"></div>
</div>
</div>
<div class="col-lg-4 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-header border-bottom">
<h5 class="card-title mb-0">Instalacje na mapie</h5>
<small class="text-muted">Wyszukaj, filtruj i zarządzaj widocznością</small>
</div>
<div class="card-body">
<div class="mb-3 mt-3">
<label class="form-label" for="mapSearchInput">Szukaj</label>
<div class="input-group input-group-merge">
<span class="input-group-text"><i class="bx bx-search"></i></span>
<input type="text" id="mapSearchInput" class="form-control" placeholder="Nazwa lub lokalizacja...">
</div>
</div>
<div class="mb-3">
<label class="form-label" for="mapTypeFilter">Typ</label>
<select id="mapTypeFilter" class="form-select">
<option value="">Wszystkie typy</option>
<option value="gas">Zbiornik Gazu</option>
<option value="farm">Kurnik / Farma</option>
</select>
</div>
<div class="mb-3">
<label class="form-label" for="mapStatusFilter">Status</label>
<select id="mapStatusFilter" class="form-select">
<option value="">Każdy status</option>
<option value="online">Online</option>
<option value="alert">Alert</option>
<option value="offline">Offline</option>
</select>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-medium">Widoczność</span>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" id="showAllInstallationsBtn">Wszystkie</button>
<button type="button" class="btn btn-outline-secondary" id="hideAllInstallationsBtn">Ukryj</button>
</div>
</div>
<div id="mapInstallationsList" class="map-sidebar-list border rounded"></div>
</div>
</div>
</div>
</div>
</div>
<?php include '../../footer.php'; ?>
<script src="<?= $basePath ?>/assets/vendor/libs/leaflet/leaflet.js"></script>
<script>
(function () {
const installations = [
{
id: 'inst-1',
name: 'Zbiornik LPG #1',
location: 'Stalowa Wola, ul. Przemysłowa 5',
type: 'gas',
typeLabel: 'Zbiornik Gazu',
status: 'online',
statusLabel: 'Online',
lat: 50.5826,
lng: 22.0534,
alert: null,
url: 'installation-live.php'
},
{
id: 'inst-2',
name: 'Kurnik B (Sektor 2)',
location: 'Farma Wola, Obiekt Główny',
type: 'farm',
typeLabel: 'Sterownik Kurnika',
status: 'online',
statusLabel: 'Online',
lat: 50.6012,
lng: 22.0918,
alert: null,
url: 'installation-live.php'
},
{
id: 'inst-3',
name: 'Zbiornik LPG #2',
location: 'Rzeszów, Baza Północ',
type: 'gas',
typeLabel: 'Zbiornik Gazu',
status: 'offline',
statusLabel: 'Offline',
lat: 50.0578,
lng: 22.0048,
alert: 'Brak łączności — brak sygnału od ponad 3 dni.',
url: 'installation-live.php'
},
{
id: 'inst-4',
name: 'Zbiornik LPG #3',
location: 'Lublin, ul. Metalurgiczna 12',
type: 'gas',
typeLabel: 'Zbiornik Gazu',
status: 'alert',
statusLabel: 'Alert',
lat: 51.2354,
lng: 22.5489,
alert: 'Niski poziom gazu — poziom poniżej 15%.',
url: 'installation-live.php'
},
{
id: 'inst-5',
name: 'Zbiornik LPG #4',
location: 'Tarnobrzeg, Stacja Przeładunkowa',
type: 'gas',
typeLabel: 'Zbiornik Gazu',
status: 'online',
statusLabel: 'Online',
lat: 50.5731,
lng: 21.6794,
alert: null,
url: 'installation-live.php'
},
{
id: 'inst-6',
name: 'Sterownik Farma Alpha',
location: 'Nisko, Sektor A',
type: 'farm',
typeLabel: 'Sterownik Kurnika',
status: 'online',
statusLabel: 'Online',
lat: 50.5198,
lng: 22.1396,
alert: null,
url: 'installation-live.php'
},
{
id: 'inst-7',
name: 'Zbiornik LPG #5',
location: 'Przemyśl, Baza Południe',
type: 'gas',
typeLabel: 'Zbiornik Gazu',
status: 'online',
statusLabel: 'Online',
lat: 49.7837,
lng: 22.7673,
alert: null,
url: 'installation-live.php'
}
];
const mapEl = document.getElementById('installationsMap');
if (!mapEl || typeof L === 'undefined') {
return;
}
const map = L.map(mapEl).setView([50.45, 22.15], 8);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 18
}).addTo(map);
const markersById = {};
let closePopupTimer = null;
function createMarkerIcon(hasAlert) {
const pinClass = hasAlert ? ' is-alert' : '';
const iconHtml = hasAlert
? '<i class="bx bx-error"></i>'
: '<i class="bx bx-buildings"></i>';
return L.divIcon({
className: '',
html: '<div class="map-marker-pin' + pinClass + '">' + iconHtml + '</div>',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -28]
});
}
function buildPopupHtml(item) {
const alertBlock = item.alert
? '<div class="alert alert-danger py-2 px-3 mb-2 small">' +
'<i class="bx bx-error-circle me-1"></i>' + item.alert +
'</div>'
: '<p class="text-muted small mb-2">Brak aktywnych alertów</p>';
return (
'<div>' +
'<h6 class="mb-1 fw-bold">' + item.name + '</h6>' +
'<small class="text-muted d-block mb-2"><i class="bx bx-map me-1"></i>' + item.location + '</small>' +
alertBlock +
'<button type="button" class="btn btn-sm btn-primary w-100" onclick="window.location.href=\'' + item.url + '\'">' +
'<i class="bx bx-right-arrow-alt me-1"></i> Przejdź do instalacji' +
'</button>' +
'</div>'
);
}
function statusBadgeClass(status) {
if (status === 'online') return 'bg-success';
if (status === 'alert') return 'bg-danger';
return 'bg-secondary';
}
installations.forEach(function (item) {
const marker = L.marker([item.lat, item.lng], {
icon: createMarkerIcon(Boolean(item.alert)),
title: item.name
});
marker.bindPopup(buildPopupHtml(item));
marker.on('mouseover', function () {
if (closePopupTimer) {
clearTimeout(closePopupTimer);
closePopupTimer = null;
}
marker.openPopup();
});
marker.on('mouseout', function () {
closePopupTimer = setTimeout(function () {
marker.closePopup();
}, 280);
});
marker.on('popupopen', function () {
const popupEl = marker.getPopup().getElement();
if (!popupEl) return;
popupEl.addEventListener('mouseenter', function () {
if (closePopupTimer) {
clearTimeout(closePopupTimer);
closePopupTimer = null;
}
});
popupEl.addEventListener('mouseleave', function () {
closePopupTimer = setTimeout(function () {
marker.closePopup();
}, 200);
});
});
markersById[item.id] = {
item: item,
marker: marker,
visible: true,
matchesFilter: true
};
marker.addTo(map);
});
const listEl = document.getElementById('mapInstallationsList');
const searchInput = document.getElementById('mapSearchInput');
const typeFilter = document.getElementById('mapTypeFilter');
const statusFilter = document.getElementById('mapStatusFilter');
function renderList() {
const fragments = installations.map(function (item) {
const state = markersById[item.id];
if (!state.matchesFilter) {
return '';
}
const hiddenClass = state.visible ? '' : ' is-hidden-on-map';
const alertIcon = item.alert
? '<i class="bx bx-error-circle text-danger ms-1" title="Alert"></i>'
: '';
return (
'<div class="map-list-item border-bottom px-3 py-2' + hiddenClass + '" data-id="' + item.id + '">' +
'<div class="d-flex align-items-start gap-2">' +
'<div class="form-check form-switch mb-0 mt-1">' +
'<input class="form-check-input map-visibility-toggle" type="checkbox" role="switch" ' +
'data-id="' + item.id + '" ' + (state.visible ? 'checked' : '') + ' ' +
'aria-label="Widoczność ' + item.name + '">' +
'</div>' +
'<div class="flex-grow-1 min-w-0">' +
'<div class="d-flex justify-content-between align-items-start gap-2">' +
'<div class="min-w-0">' +
'<div class="fw-semibold text-truncate">' + item.name + alertIcon + '</div>' +
'<small class="text-muted text-truncate d-block">' + item.location + '</small>' +
'</div>' +
'<span class="badge ' + statusBadgeClass(item.status) + '">' + item.statusLabel + '</span>' +
'</div>' +
'<small class="text-muted">' + item.typeLabel + '</small>' +
'</div>' +
'</div>' +
'</div>'
);
}).join('');
listEl.innerHTML = fragments ||
'<div class="text-center text-muted py-4 px-3 small">Brak instalacji spełniających kryteria</div>';
}
function applyFilters() {
const query = (searchInput.value || '').trim().toLowerCase();
const type = typeFilter.value;
const status = statusFilter.value;
installations.forEach(function (item) {
const state = markersById[item.id];
const haystack = (item.name + ' ' + item.location).toLowerCase();
const matchesQuery = !query || haystack.indexOf(query) !== -1;
const matchesType = !type || item.type === type;
const matchesStatus = !status || item.status === status;
state.matchesFilter = matchesQuery && matchesType && matchesStatus;
const shouldShow = state.matchesFilter && state.visible;
if (shouldShow) {
if (!map.hasLayer(state.marker)) {
state.marker.addTo(map);
}
} else if (map.hasLayer(state.marker)) {
map.removeLayer(state.marker);
}
});
renderList();
}
function setAllVisibility(visible) {
installations.forEach(function (item) {
markersById[item.id].visible = visible;
});
applyFilters();
}
function focusInstallation(id) {
const state = markersById[id];
if (!state || !state.matchesFilter) return;
if (!state.visible) {
state.visible = true;
applyFilters();
}
map.flyTo([state.item.lat, state.item.lng], 12, { duration: 0.6 });
state.marker.openPopup();
listEl.querySelectorAll('.map-list-item').forEach(function (el) {
el.classList.toggle('is-focused', el.getAttribute('data-id') === id);
});
}
listEl.addEventListener('change', function (event) {
const toggle = event.target.closest('.map-visibility-toggle');
if (!toggle) return;
const id = toggle.getAttribute('data-id');
markersById[id].visible = toggle.checked;
applyFilters();
});
listEl.addEventListener('click', function (event) {
if (event.target.closest('.map-visibility-toggle')) return;
const row = event.target.closest('.map-list-item');
if (!row) return;
focusInstallation(row.getAttribute('data-id'));
});
searchInput.addEventListener('input', applyFilters);
typeFilter.addEventListener('change', applyFilters);
statusFilter.addEventListener('change', applyFilters);
document.getElementById('showAllInstallationsBtn').addEventListener('click', function () {
setAllVisibility(true);
});
document.getElementById('hideAllInstallationsBtn').addEventListener('click', function () {
setAllVisibility(false);
});
applyFilters();
setTimeout(function () {
map.invalidateSize();
}, 200);
})();
</script>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
<?php
$enablePrototypeComments = true;
include '../../header-telemetria.php';
?>
<div class="container-xxl flex-grow-1 container-p-y">
<h4 class="fw-bold py-3 mb-4">
Telemetria <span class="text-muted fw-light">/ Logi systemowe</span>
</h4>
<div class="card shadow-sm border-0">
<div class="card-header border-bottom">
<h5 class="card-title mb-0">Dziennik zdarzeń (Logi modułu)</h5>
</div>
<div class="table-responsive text-nowrap">
<table class="table table-hover">
<thead>
<tr>
<th>Data</th>
<th>Użytkownik</th>
<th>Operacja</th>
<th>Szczegóły</th>
</tr>
</thead>
<tbody class="table-border-bottom-0">
<tr>
<td>2026-06-19 16:30</td>
<td><span class="badge bg-label-primary"><i class="bx bx-user me-1"></i> Jan Kowalski</span></td>
<td><span class="fw-bold text-danger">Usunięcie urządzenia</span></td>
<td>ID: 1024</td>
</tr>
<tr>
<td>2026-06-19 16:20</td>
<td><span class="badge bg-label-secondary"><i class="bx bx-cog me-1"></i> System</span></td>
<td><span class="fw-bold text-warning">Zmiana statusu</span></td>
<td>ID: 1024, Status: Online → Offline (utrata łączności)</td>
</tr>
<tr>
<td>2026-06-19 16:15</td>
<td><span class="badge bg-label-primary"><i class="bx bx-user me-1"></i> Jan Kowalski</span></td>
<td><span class="fw-bold text-info">Edycja danych</span></td>
<td>ID: 1024, Edycja danych urządzenia.</td>
</tr>
<tr>
<td>2026-06-19 16:10</td>
<td><span class="badge bg-label-primary"><i class="bx bx-user me-1"></i> Jan Kowalski</span></td>
<td><span class="fw-bold text-success">Dodanie urządzenia</span></td>
<td>MAC: AA:BB:CC:DD:EE:FF, Nazwa: "Sterownik Klimatu Alpha"</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="content-backdrop fade"></div>
<?php include '../../footer.php'; ?>
</body>
</html>
+32 -2
View File
@@ -42,11 +42,23 @@ $currentPage = basename($_SERVER['PHP_SELF']);
</li>
</ul>
</li>
<li class="menu-item <?php echo ($currentPage == 'installations.php') ? 'active' : ''; ?>">
<a href="installations.php" class="menu-link">
<li class="menu-item <?php echo in_array($currentPage, ['installations.php', 'installations-map.php'], true) ? 'active open' : ''; ?>">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon tf-icons bx bx-buildings"></i>
<div class="text-truncate" data-i18n="Instalacje">Instalacje</div>
</a>
<ul class="menu-sub">
<li class="menu-item <?php echo ($currentPage == 'installations.php') ? 'active' : ''; ?>">
<a href="installations.php" class="menu-link">
<div class="text-truncate" data-i18n="Lista">Lista instalacji</div>
</a>
</li>
<li class="menu-item <?php echo ($currentPage == 'installations-map.php') ? 'active' : ''; ?>">
<a href="installations-map.php" class="menu-link">
<div class="text-truncate" data-i18n="Mapa instalacji">Mapa instalacji</div>
</a>
</li>
</ul>
</li>
<li class="menu-item <?php echo ($currentPage == 'devices.php') ? 'active' : ''; ?>">
<a href="devices.php" class="menu-link">
@@ -60,5 +72,23 @@ $currentPage = basename($_SERVER['PHP_SELF']);
<div class="text-truncate" data-i18n="Raporty">Raporty</div>
</a>
</li>
<li class="menu-item <?php echo ($currentPage == 'settings.php' || $currentPage == 'logs.php') ? 'active open' : ''; ?>">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon tf-icons bx bx-server"></i>
<div class="text-truncate" data-i18n="System">System</div>
</a>
<ul class="menu-sub">
<li class="menu-item <?php echo ($currentPage == 'settings.php') ? 'active' : ''; ?>">
<a href="settings.php" class="menu-link">
<div class="text-truncate" data-i18n="Ustawienia">Ustawienia</div>
</a>
</li>
<li class="menu-item <?php echo ($currentPage == 'logs.php') ? 'active' : ''; ?>">
<a href="logs.php" class="menu-link">
<div class="text-truncate" data-i18n="Logi">Logi systemowe</div>
</a>
</li>
</ul>
</li>
</ul>
</aside>
+90
View File
@@ -0,0 +1,90 @@
<?php
$enablePrototypeComments = true;
include '../../header-telemetria.php';
?>
<div class="container-xxl flex-grow-1 container-p-y">
<h4 class="fw-bold py-3 mb-4">
Telemetria <span class="text-muted fw-light">/ Ustawienia</span>
</h4>
<div class="row">
<div class="col-md-6 col-12 mb-4">
<div class="card shadow-sm border-0">
<div class="card-header border-bottom">
<h5 class="card-title mb-0">Powiadomienia i Alerty</h5>
</div>
<div class="card-body pt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h6 class="mb-1">Aktywny moduł alertów</h6>
<small class="text-muted">Pozwala na ustawianie zaawansowanych wyzwalaczy i powiadomień SMS/Email po przekroczeniu progów alarmowych w instalacjach.</small>
</div>
<div class="form-check form-switch form-switch-lg ms-3 mb-0">
<input class="form-check-input" type="checkbox" id="alertsModuleSwitch">
</div>
</div>
<button type="button" class="btn btn-primary mt-3" onclick="saveSettings()">Zapisz ustawienia</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal potwierdzenia włączenia -->
<div class="modal fade" id="confirmPaymentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-warning border-2">
<div class="modal-header">
<h5 class="modal-title text-warning"><i class="bx bx-error-circle me-1"></i> Potwierdzenie subskrypcji</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="cancelSwitch()"></button>
</div>
<div class="modal-body">
<p>Uruchomienie modułu <strong>Powiadomienia i Alerty</strong> wiąże się z dodatkową opłatą abonamentową w wysokości <strong>50 zł / msc</strong> doliczaną do Twojej faktury.</p>
<p class="mb-0 text-dark fw-bold">Czy na pewno chcesz włączyć ten moduł?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" onclick="cancelSwitch()">Anuluj</button>
<button type="button" class="btn btn-success" data-bs-dismiss="modal" onclick="confirmSave()">Tak, włączam moduł</button>
</div>
</div>
</div>
</div>
<script>
let isCurrentlyEnabled = false; // Zakładamy że na start jest wyłączone
function saveSettings() {
const switchEl = document.getElementById('alertsModuleSwitch');
// Jeśli chcemy włączyć, a wcześniej było wyłączone
if (switchEl.checked && !isCurrentlyEnabled) {
var paymentModal = new bootstrap.Modal(document.getElementById('confirmPaymentModal'));
paymentModal.show();
} else if (!switchEl.checked && isCurrentlyEnabled) {
alert('Moduł został pomyślnie wyłączony. Opłata nie będzie naliczana od następnego okresu rozliczeniowego.');
isCurrentlyEnabled = false;
} else {
alert('Ustawienia zapisane bez zmian.');
}
}
function confirmSave() {
isCurrentlyEnabled = true;
document.getElementById('alertsModuleSwitch').checked = true;
alert('Moduł został włączony! Doliczono 50 zł/msc do Twojego abonamentu. Możesz teraz konfigurować alerty w instalacjach.');
}
function cancelSwitch() {
// Jeśli anulowano modal, przywracamy przełącznik do stanu wylączonego
document.getElementById('alertsModuleSwitch').checked = false;
}
</script>
<div class="content-backdrop fade"></div>
<?php include '../../footer.php'; ?>
</body>
</html>