2026-05-22 23:05:37 +02:00

544 lines
19 KiB
JavaScript

const state = {
user: null,
devices: [],
dashboardElevators: [],
countdownIntervalId: null,
activeModal: null,
selectedLogElevatorId: null
};
const elements = {
welcomeText: document.getElementById("welcomeText"),
routeText: document.getElementById("routeText"),
logoutButton: document.getElementById("logoutButton"),
dashboardCards: document.getElementById("dashboardCards"),
deviceList: document.getElementById("deviceList"),
openDeviceModalButton: document.getElementById("openDeviceModalButton"),
openLogModalButton: document.getElementById("openLogModalButton"),
deviceModal: document.getElementById("deviceModal"),
logModal: document.getElementById("logModal"),
deviceForm: document.getElementById("deviceForm"),
deviceEditId: document.getElementById("deviceEditId"),
deviceModalTitle: document.getElementById("deviceModalTitle"),
deviceModalSubtitle: document.getElementById("deviceModalSubtitle"),
deviceRouteName: document.getElementById("deviceRouteName"),
deviceName: document.getElementById("deviceName"),
deviceSerialNumber: document.getElementById("deviceSerialNumber"),
deviceLocation: document.getElementById("deviceLocation"),
deviceStatus: document.getElementById("deviceStatus"),
deviceInspectionIntervalDays: document.getElementById("deviceInspectionIntervalDays"),
deviceNextInspectionDate: document.getElementById("deviceNextInspectionDate"),
deviceMessage: document.getElementById("deviceMessage"),
closeDeviceModalButton: document.getElementById("closeDeviceModalButton"),
cancelDeviceModalButton: document.getElementById("cancelDeviceModalButton"),
logForm: document.getElementById("logForm"),
logElevatorId: document.getElementById("logElevatorId"),
logElevatorSummary: document.getElementById("logElevatorSummary"),
logStatus: document.getElementById("logStatus"),
logInspectionDate: document.getElementById("logInspectionDate"),
logInspectionIntervalDays: document.getElementById("logInspectionIntervalDays"),
logNextInspectionDate: document.getElementById("logNextInspectionDate"),
logMessage: document.getElementById("logMessage"),
closeLogModalButton: document.getElementById("closeLogModalButton"),
cancelLogModalButton: document.getElementById("cancelLogModalButton"),
openDeviceFromLogButton: document.getElementById("openDeviceFromLogButton")
};
bindEvents();
bootPage();
function bindEvents() {
elements.logoutButton.addEventListener("click", logout);
elements.openDeviceModalButton.addEventListener("click", () => openDeviceModal());
elements.openLogModalButton.addEventListener("click", () => openLogModal());
elements.deviceForm.addEventListener("submit", handleDeviceSave);
elements.logForm.addEventListener("submit", handleLogSave);
elements.closeDeviceModalButton.addEventListener("click", closeActiveModal);
elements.cancelDeviceModalButton.addEventListener("click", closeActiveModal);
elements.closeLogModalButton.addEventListener("click", closeActiveModal);
elements.cancelLogModalButton.addEventListener("click", closeActiveModal);
elements.openDeviceFromLogButton.addEventListener("click", handleOpenDeviceFromLog);
elements.logElevatorId.addEventListener("change", handleLogElevatorChange);
elements.logInspectionDate.addEventListener("change", syncLogNextInspectionDate);
elements.logInspectionIntervalDays.addEventListener("input", syncLogNextInspectionDate);
elements.deviceList.addEventListener("click", handleDeviceListClick);
elements.deviceModal.addEventListener("click", handleModalBackdropClick);
elements.logModal.addEventListener("click", handleModalBackdropClick);
window.addEventListener("keydown", handleWindowKeydown);
window.addEventListener("pageshow", handlePageShow);
}
async function bootPage() {
setToday(elements.logInspectionDate);
syncLogNextInspectionDate();
renderDevices();
renderDashboard();
try {
const response = await apiFetch("/api/me");
state.user = response.user;
fillUserHeader();
elements.deviceRouteName.value = state.user?.routeName || "";
await Promise.allSettled([loadDashboard(), loadDevices()]);
} catch (error) {
if (error.status === 401) {
window.location.href = "/bejelentkezes";
return;
}
elements.welcomeText.textContent = "Betöltési hiba";
elements.routeText.textContent = "A munkamenet ellenőrzése nem sikerült. Frissítsd az oldalt vagy jelentkezz be újra.";
setFeedback(elements.logMessage, error.message, "error");
}
}
function fillUserHeader() {
const name = state.user?.fullName || "Felhasználó";
const route = state.user?.routeName || "Nincs megadva";
elements.welcomeText.textContent = "Kezelőfelület";
elements.routeText.textContent = `${name} · Technikusi útvonal: ${route}`;
}
async function loadDashboard() {
try {
const response = await apiFetch("/api/dashboard");
state.dashboardElevators = response.elevators || [];
renderDashboard();
} catch (error) {
state.dashboardElevators = [];
renderDashboard();
setFeedback(elements.logMessage, error.message, "error");
}
}
async function loadDevices() {
try {
const response = await apiFetch("/api/elevators");
state.devices = response.elevators || [];
renderDevices();
renderLogElevatorOptions(state.selectedLogElevatorId);
} catch (error) {
state.devices = [];
renderDevices();
renderLogElevatorOptions(null);
setFeedback(elements.deviceMessage, error.message, "error");
}
}
function renderDashboard() {
if (!state.dashboardElevators.length) {
elements.dashboardCards.innerHTML = `
<article class="dashboard-card empty-card">
<h3>Nincs közelgő határidő</h3>
<p>A következő 7 napban esedékes ellenőrzések itt jelennek meg.</p>
</article>
`;
clearCountdownTimer();
return;
}
elements.dashboardCards.innerHTML = state.dashboardElevators
.map((elevator) => {
const countdown = calculateCountdown(elevator.nextInspectionDate);
const statusClass = countdown.expired ? "danger-chip" : countdown.warning ? "warning-chip" : "success-chip";
return `
<article class="dashboard-card">
<div class="card-topline">
<span class="status-chip ${statusClass}">${countdown.label}</span>
<span class="serial-chip">${escapeHtml(elevator.serialNumber)}</span>
</div>
<h3>${escapeHtml(elevator.name)}</h3>
<p>${escapeHtml(elevator.location)}</p>
<p>Legutóbbi állapot: ${humanizeStatus(elevator.lastStatus)}</p>
<div class="countdown-box" data-next-date="${elevator.nextInspectionDate}">
${countdown.text}
</div>
<p class="muted-text">Következő ellenőrzés: ${formatDate(elevator.nextInspectionDate)}</p>
</article>
`;
})
.join("");
clearCountdownTimer();
state.countdownIntervalId = window.setInterval(updateCountdownBoxes, 1000);
}
function renderDevices() {
if (!state.devices.length) {
elements.deviceList.innerHTML = `
<article class="entry-card empty-card">
<h3>Még nincs rögzített eszköz</h3>
<p>Hozz létre egy új eszközt, majd adj hozzá naplóbejegyzéseket az állapot frissítéséhez.</p>
</article>
`;
elements.openLogModalButton.disabled = true;
return;
}
elements.openLogModalButton.disabled = false;
elements.deviceList.innerHTML = state.devices
.map((device) => `
<article class="entry-card device-card">
<div class="card-topline">
<span class="status-chip ${statusClassName(device.lastStatus)}">${humanizeStatus(device.lastStatus)}</span>
<span class="serial-chip">${escapeHtml(device.serialNumber)}</span>
</div>
<h3>${escapeHtml(device.name)}</h3>
<p><strong>Helyszín:</strong> ${escapeHtml(device.location)}</p>
<p><strong>Útvonal:</strong> ${escapeHtml(device.routeName)}</p>
<p><strong>Utolsó ellenőrzés:</strong> ${formatDate(device.lastInspectionDate)}</p>
<p><strong>Következő ellenőrzés:</strong> ${formatDate(device.nextInspectionDate)}</p>
<p><strong>Ciklus:</strong> ${escapeHtml(device.inspectionIntervalDays)} nap</p>
<div class="device-actions">
<button class="secondary-button" type="button" data-action="log" data-elevator-id="${device.id}">Új napló</button>
<button class="ghost-button" type="button" data-action="edit" data-elevator-id="${device.id}">Szerkesztés</button>
</div>
</article>
`)
.join("");
}
function renderLogElevatorOptions(preferredElevatorId) {
if (!state.devices.length) {
state.selectedLogElevatorId = null;
elements.logElevatorId.innerHTML = `<option value="">Előbb hozz létre egy eszközt</option>`;
elements.logElevatorSummary.textContent = "A naplózáshoz előbb egy eszközt kell létrehozni.";
return;
}
const fallbackId = preferredElevatorId || state.selectedLogElevatorId || state.devices[0].id;
const resolvedId = state.devices.some((device) => device.id === Number(fallbackId)) ? Number(fallbackId) : state.devices[0].id;
state.selectedLogElevatorId = resolvedId;
elements.logElevatorId.innerHTML = state.devices
.map(
(device) => `
<option value="${device.id}" ${device.id === resolvedId ? "selected" : ""}>
${escapeHtml(device.name)} · ${escapeHtml(device.serialNumber)}
</option>
`
)
.join("");
renderSelectedElevatorSummary(resolvedId);
}
function renderSelectedElevatorSummary(elevatorId) {
const device = state.devices.find((entry) => entry.id === Number(elevatorId));
if (!device) {
elements.logElevatorSummary.textContent = "Válassz ki egy eszközt a naplóbejegyzéshez.";
return;
}
elements.logElevatorSummary.innerHTML = `
<strong>${escapeHtml(device.name)}</strong><br />
Gyári szám: ${escapeHtml(device.serialNumber)}<br />
Helyszín: ${escapeHtml(device.location)}<br />
Utolsó állapot: ${humanizeStatus(device.lastStatus)}<br />
Következő ellenőrzés: ${formatDate(device.nextInspectionDate)}
`;
}
function updateCountdownBoxes() {
document.querySelectorAll(".countdown-box").forEach((box) => {
const countdown = calculateCountdown(box.dataset.nextDate);
box.textContent = countdown.text;
});
}
function clearCountdownTimer() {
if (state.countdownIntervalId) {
window.clearInterval(state.countdownIntervalId);
state.countdownIntervalId = null;
}
}
function handlePageShow() {
if (state.user) {
Promise.allSettled([loadDashboard(), loadDevices()]);
}
}
function handleDeviceListClick(event) {
const button = event.target.closest("button[data-action]");
if (!button) {
return;
}
const elevatorId = Number(button.dataset.elevatorId);
if (button.dataset.action === "log") {
openLogModal(elevatorId);
return;
}
if (button.dataset.action === "edit") {
openDeviceModal(elevatorId);
}
}
function handleLogElevatorChange() {
const elevatorId = Number(elements.logElevatorId.value);
state.selectedLogElevatorId = Number.isInteger(elevatorId) && elevatorId > 0 ? elevatorId : null;
renderSelectedElevatorSummary(state.selectedLogElevatorId);
const device = getSelectedElevator();
if (device) {
elements.logInspectionIntervalDays.value = device.inspectionIntervalDays || 30;
syncLogNextInspectionDate();
}
}
function handleModalBackdropClick(event) {
if (event.target === event.currentTarget) {
closeActiveModal();
}
}
function handleWindowKeydown(event) {
if (event.key === "Escape" && state.activeModal) {
closeActiveModal();
}
}
function handleOpenDeviceFromLog() {
closeActiveModal();
openDeviceModal(null, true);
}
async function handleDeviceSave(event) {
event.preventDefault();
const elevatorId = Number(elements.deviceEditId.value || 0);
const payload = {
name: elements.deviceName.value,
serialNumber: elements.deviceSerialNumber.value,
location: elements.deviceLocation.value,
routeName: elements.deviceRouteName.value,
status: elements.deviceStatus.value,
inspectionIntervalDays: elements.deviceInspectionIntervalDays.value,
nextInspectionDate: elements.deviceNextInspectionDate.value || "",
lastInspectionDate: elements.deviceForm.dataset.lastInspectionDate || ""
};
try {
const response = await apiFetch(elevatorId ? `/api/elevators/${elevatorId}` : "/api/elevators", {
method: elevatorId ? "PUT" : "POST",
body: JSON.stringify(payload)
});
setFeedback(elements.deviceMessage, response.message, "success");
await Promise.allSettled([loadDevices(), loadDashboard()]);
closeActiveModal();
if (elements.deviceForm.dataset.returnToLog === "true" && response.elevator?.id) {
openLogModal(response.elevator.id);
}
} catch (error) {
setFeedback(elements.deviceMessage, error.message, "error");
}
}
async function handleLogSave(event) {
event.preventDefault();
const formData = new FormData(elements.logForm);
try {
const response = await apiFetch("/api/logs", {
method: "POST",
body: formData,
isFormData: true
});
setFeedback(elements.logMessage, response.message, "success");
await Promise.allSettled([loadDashboard(), loadDevices()]);
closeActiveModal();
} catch (error) {
setFeedback(elements.logMessage, error.message, "error");
}
}
async function logout() {
try {
const response = await apiFetch("/api/logout", { method: "POST" });
window.location.href = response.redirectPath;
} catch (_error) {
window.location.href = "/bejelentkezes";
}
}
function openDeviceModal(elevatorId = null, returnToLog = false) {
const device = state.devices.find((entry) => entry.id === Number(elevatorId));
elements.deviceForm.reset();
elements.deviceForm.dataset.returnToLog = returnToLog ? "true" : "false";
elements.deviceForm.dataset.lastInspectionDate = normalizeDateInput(device?.lastInspectionDate);
elements.deviceEditId.value = device?.id || "";
elements.deviceRouteName.value = state.user?.routeName || "";
elements.deviceInspectionIntervalDays.value = device?.inspectionIntervalDays || 30;
elements.deviceStatus.value = device?.lastStatus || "rendben";
elements.deviceNextInspectionDate.value = normalizeDateInput(device?.nextInspectionDate);
elements.deviceName.value = device?.name || "";
elements.deviceSerialNumber.value = device?.serialNumber || "";
elements.deviceLocation.value = device?.location || "";
elements.deviceModalTitle.textContent = device ? "Eszköz szerkesztése" : "Új eszköz";
elements.deviceModalSubtitle.textContent = device
? "Az eszköz alapadatainak frissítése. Az állapotot naplóbejegyzéssel is módosíthatod."
: "Új lift vagy berendezés rögzítése a technikusi útvonalhoz.";
setFeedback(elements.deviceMessage, "", "");
showModal(elements.deviceModal);
}
function openLogModal(elevatorId = null) {
if (!state.devices.length) {
openDeviceModal();
setFeedback(elements.deviceMessage, "A naplózáshoz előbb létre kell hoznod egy eszközt.", "error");
return;
}
elements.logForm.reset();
setFeedback(elements.logMessage, "", "");
setToday(elements.logInspectionDate);
elements.logStatus.value = "rendben";
renderLogElevatorOptions(elevatorId);
elements.logElevatorId.value = String(state.selectedLogElevatorId || "");
handleLogElevatorChange();
showModal(elements.logModal);
}
function showModal(modalElement) {
closeActiveModal();
state.activeModal = modalElement;
modalElement.classList.remove("hidden");
document.body.classList.add("modal-open");
}
function closeActiveModal() {
if (!state.activeModal) {
return;
}
state.activeModal.classList.add("hidden");
state.activeModal = null;
document.body.classList.remove("modal-open");
}
function getSelectedElevator() {
return state.devices.find((device) => device.id === Number(state.selectedLogElevatorId)) || null;
}
async function apiFetch(url, options = {}) {
const headers = options.isFormData ? {} : { "Content-Type": "application/json" };
const response = await fetch(url, {
method: options.method || "GET",
credentials: "include",
headers,
body: options.body
});
const contentType = response.headers.get("content-type") || "";
const data = contentType.includes("application/json")
? await response.json()
: { message: "A szerver nem JSON választ adott vissza." };
if (!response.ok) {
const error = new Error(data.message || "A kérés nem sikerült.");
error.status = response.status;
throw error;
}
return data;
}
function syncLogNextInspectionDate() {
const inspectionDate = elements.logInspectionDate.value;
const intervalDays = Number(elements.logInspectionIntervalDays.value || 30);
if (!inspectionDate) {
return;
}
const baseDate = new Date(`${inspectionDate}T00:00:00`);
baseDate.setDate(baseDate.getDate() + intervalDays);
elements.logNextInspectionDate.value = baseDate.toISOString().slice(0, 10);
}
function setToday(element) {
element.value = new Date().toISOString().slice(0, 10);
}
function calculateCountdown(nextInspectionDate) {
const normalizedDate = normalizeDateInput(nextInspectionDate);
if (!normalizedDate) {
return {
expired: false,
warning: false,
label: "Nincs dátum",
text: "Nincs megadott határidő"
};
}
const targetTime = new Date(`${normalizedDate}T23:59:59`).getTime();
const diff = targetTime - Date.now();
const expired = diff < 0;
const absoluteDiff = Math.abs(diff);
const totalHours = Math.floor(absoluteDiff / (1000 * 60 * 60));
const days = Math.floor(totalHours / 24);
const hours = totalHours % 24;
const warning = !expired && days <= 7;
const label = expired ? "Lejárt" : warning ? "Hamarosan esedékes" : "Rendben";
const prefix = expired ? "Túllépés" : "Hátralévő idő";
return {
expired,
warning,
label,
text: `${prefix}: ${days} nap ${hours} óra`
};
}
function humanizeStatus(status) {
return status === "rendben" ? "Rendben" : status === "figyelmeztetes" ? "Figyelmeztetés" : "Lejárt";
}
function statusClassName(status) {
return status === "rendben" ? "success-chip" : status === "figyelmeztetes" ? "warning-chip" : "danger-chip";
}
function formatDate(value) {
const normalizedDate = normalizeDateInput(value);
return normalizedDate ? new Date(`${normalizedDate}T00:00:00`).toLocaleDateString("hu-HU") : "Nincs dátum";
}
function normalizeDateInput(value) {
if (!value) {
return "";
}
if (typeof value === "string") {
return value.includes("T") ? value.slice(0, 10) : value;
}
const parsedDate = new Date(value);
return Number.isNaN(parsedDate.getTime()) ? "" : parsedDate.toISOString().slice(0, 10);
}
function setFeedback(element, message, type) {
element.textContent = message;
element.className = `feedback-text ${type ? `feedback-${type}` : ""}`;
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}