544 lines
19 KiB
JavaScript
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("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|