first commit
This commit is contained in:
+189
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Alkalmazás | LiftEllenőr</title>
|
||||
<link rel="stylesheet" href="/style.css?v=20260522-5" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="pageShell">
|
||||
<main id="appShell">
|
||||
<div id="topBar" class="panel app-topbar">
|
||||
<div class="topbar-heading">
|
||||
<img class="topbar-logo" src="/logo.png" alt="" aria-hidden="true" />
|
||||
<div>
|
||||
<h1 id="welcomeText" class="page-title">Kezelőfelület</h1>
|
||||
<p id="routeText" class="page-text"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<a class="ghost-button" href="/naplo">Napló megnyitása</a>
|
||||
<button id="logoutButton" class="ghost-button" type="button">Kijelentkezés</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="section-heading">
|
||||
<h2>Határidők</h2>
|
||||
<p>A bejelentkezett technikus útvonalához tartozó, a következő 7 napban esedékes ellenőrzések.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dashboardCards" class="dashboard-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="section-heading">
|
||||
<h2>Eszközök</h2>
|
||||
<p>Külön kezelt eszközlista, amelynek az állapota új naplóbejegyzésekkel frissül.</p>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<button id="openDeviceModalButton" class="secondary-button" type="button">Új eszköz</button>
|
||||
<button id="openLogModalButton" class="primary-button" type="button">Új napló</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="deviceList" class="entries-grid"></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="deviceModal" class="modal-backdrop hidden">
|
||||
<div class="modal-panel" role="dialog" aria-modal="true" aria-labelledby="deviceModalTitle">
|
||||
<div class="modal-head">
|
||||
<div class="section-heading">
|
||||
<h2 id="deviceModalTitle">Új eszköz</h2>
|
||||
<p id="deviceModalSubtitle">Új lift vagy berendezés rögzítése a technikusi útvonalhoz.</p>
|
||||
</div>
|
||||
<button id="closeDeviceModalButton" class="ghost-button modal-close" type="button">Bezárás</button>
|
||||
</div>
|
||||
|
||||
<form id="deviceForm">
|
||||
<input id="deviceEditId" name="elevatorId" type="hidden" />
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field-wrap">
|
||||
<label for="deviceName">Eszköz neve</label>
|
||||
<input id="deviceName" name="name" type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="deviceSerialNumber">Gyári szám</label>
|
||||
<input id="deviceSerialNumber" name="serialNumber" type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="field-wrap field-wide">
|
||||
<label for="deviceLocation">Helyszín</label>
|
||||
<input id="deviceLocation" name="location" type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="deviceRouteName">Technikusi útvonal</label>
|
||||
<input id="deviceRouteName" class="readonly-input" name="routeName" type="text" readonly />
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="deviceStatus">Aktuális állapot</label>
|
||||
<select id="deviceStatus" name="status">
|
||||
<option value="rendben">Rendben</option>
|
||||
<option value="figyelmeztetes">Figyelmeztetés</option>
|
||||
<option value="lejart">Lejárt</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="deviceInspectionIntervalDays">Ellenőrzési ciklus napokban</label>
|
||||
<input id="deviceInspectionIntervalDays" name="inspectionIntervalDays" type="number" min="1" value="30" required />
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="deviceNextInspectionDate">Következő ellenőrzés</label>
|
||||
<input id="deviceNextInspectionDate" name="nextInspectionDate" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<div class="inline-actions">
|
||||
<button id="deviceSubmitButton" class="primary-button" type="submit">Eszköz mentése</button>
|
||||
<button id="cancelDeviceModalButton" class="ghost-button" type="button">Mégse</button>
|
||||
</div>
|
||||
<p id="deviceMessage" class="feedback-text"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="logModal" class="modal-backdrop hidden">
|
||||
<div class="modal-panel" role="dialog" aria-modal="true" aria-labelledby="logModalTitle">
|
||||
<div class="modal-head">
|
||||
<div class="section-heading">
|
||||
<h2 id="logModalTitle">Új naplóbejegyzés</h2>
|
||||
<p>Egy meglévő eszköz állapotának frissítése új ellenőrzési naplóval.</p>
|
||||
</div>
|
||||
<button id="closeLogModalButton" class="ghost-button modal-close" type="button">Bezárás</button>
|
||||
</div>
|
||||
|
||||
<form id="logForm">
|
||||
<div class="form-grid">
|
||||
<div class="field-wrap field-wide">
|
||||
<label for="logElevatorId">Eszköz kiválasztása</label>
|
||||
<select id="logElevatorId" name="elevatorId" required></select>
|
||||
</div>
|
||||
|
||||
<div class="field-wrap field-wide">
|
||||
<div id="logElevatorSummary" class="summary-card">
|
||||
Válassz ki egy eszközt a naplóbejegyzéshez.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="logStatus">Állapot</label>
|
||||
<select id="logStatus" name="status">
|
||||
<option value="rendben">Rendben</option>
|
||||
<option value="figyelmeztetes">Figyelmeztetés</option>
|
||||
<option value="lejart">Lejárt</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="logInspectionDate">Ellenőrzés dátuma</label>
|
||||
<input id="logInspectionDate" name="inspectionDate" type="date" required />
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="logInspectionIntervalDays">Ellenőrzési ciklus napokban</label>
|
||||
<input id="logInspectionIntervalDays" name="inspectionIntervalDays" type="number" min="1" value="30" required />
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="logNextInspectionDate">Következő ellenőrzés</label>
|
||||
<input id="logNextInspectionDate" name="nextInspectionDate" type="date" required />
|
||||
</div>
|
||||
|
||||
<div class="field-wrap field-wide">
|
||||
<label for="logNotes">Megjegyzés</label>
|
||||
<textarea id="logNotes" name="notes" rows="4" placeholder="Rövid állapotleírás vagy teendő"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field-wrap field-wide">
|
||||
<label for="logPdfs">PDF csatolmányok</label>
|
||||
<input id="logPdfs" name="pdfs" type="file" accept="application/pdf,.pdf" multiple />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<div class="inline-actions">
|
||||
<button id="logSubmitButton" class="primary-button" type="submit">Bejegyzés mentése</button>
|
||||
<button id="openDeviceFromLogButton" class="secondary-button" type="button">Új eszköz létrehozása</button>
|
||||
<button id="cancelLogModalButton" class="ghost-button" type="button">Mégse</button>
|
||||
</div>
|
||||
<p id="logMessage" class="feedback-text"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/app.js?v=20260522-5"></script>
|
||||
</body>
|
||||
</html>
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
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("'", "'");
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
const form = document.getElementById("authForm");
|
||||
const feedback = document.getElementById("authMessage");
|
||||
const isRegisterPage = window.location.pathname === "/regisztracio";
|
||||
|
||||
form?.addEventListener("submit", handleSubmit);
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const payload = Object.fromEntries(formData.entries());
|
||||
const endpoint = isRegisterPage ? "/api/register" : "/api/login";
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || "A kérés nem sikerült.");
|
||||
}
|
||||
|
||||
setFeedback(data.message, "success");
|
||||
window.setTimeout(() => {
|
||||
window.location.href = data.redirectPath;
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
setFeedback(error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function setFeedback(message, type) {
|
||||
feedback.textContent = message;
|
||||
feedback.className = `feedback-text ${type ? `feedback-${type}` : ""}`;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Csödényi Felvonó Felülvizsgáló | Biztonság és szakértelem</title>
|
||||
<link rel="stylesheet" href="/style.css?v=20260522-3" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="pageShell">
|
||||
<header id="siteHeader" class="panel landing-header">
|
||||
<a class="brand-block brand-link" href="/">
|
||||
<img class="brand-logo" src="/logo.png" alt="Csödényi Felvonó Felülvizsgáló logó" />
|
||||
<div class="brand-copy">
|
||||
<p class="brand-name">Csödényi Felvonó Felülvizsgáló</p>
|
||||
<p class="brand-subtitle">Időszakos vizsgálatok, biztonságtechnikai ellenőrzések és szakértői támogatás.</p>
|
||||
</div>
|
||||
</a>
|
||||
<nav class="top-nav" aria-label="Fő navigáció">
|
||||
<a class="nav-link landing-login-link" href="/bejelentkezes">Technikusi belépés</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="mainLayout" class="landing-layout">
|
||||
<section class="landing-hero panel">
|
||||
<div class="landing-hero-copy">
|
||||
<p class="landing-kicker">Felvonó-felülvizsgálat kompromisszumok nélkül</p>
|
||||
<h1 id="mainTitle" class="landing-title">Biztonság és szakértelem a legfelsőbb szinteken</h1>
|
||||
<p id="heroText" class="landing-lead">
|
||||
A lift nem pusztán kényelmi eszköz, hanem a mindennapi biztonság és a zavartalan közlekedés alapja.
|
||||
A Csödényi Felvonó Felülvizsgálónál azért dolgozunk, hogy az Ön épületében üzemelő felvonók
|
||||
megbízhatóan, szabályosan és kiszámíthatóan működjenek.
|
||||
</p>
|
||||
<p class="landing-lead">
|
||||
Több éves szakmai tapasztalattal, naprakész jogszabályi ismeretekkel és korszerű diagnosztikai
|
||||
eszközökkel végezzük személy- és teherfelvonók időszakos vizsgálatát, biztonságtechnikai ellenőrzését
|
||||
és üzembe helyezés előtti felülvizsgálatát.
|
||||
</p>
|
||||
</div>
|
||||
<img class="landing-hero-logo" src="/logo.png" alt="Csödényi Felvonó Felülvizsgáló logó" />
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="section-heading">
|
||||
<h2>Miért minket válasszon?</h2>
|
||||
<p>Megbízható partner társasházak, irodaházak és ipari létesítmények számára.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="landing-feature-grid">
|
||||
<article class="landing-feature-card">
|
||||
<h3>Maximális precizitás</h3>
|
||||
<p>A biztonság területén nem ismerünk kompromisszumot: minden ellenőrzést részletesen, dokumentáltan végzünk el.</p>
|
||||
</article>
|
||||
<article class="landing-feature-card">
|
||||
<h3>Gyors és rugalmas ügyintézés</h3>
|
||||
<p>Az épület működéséhez igazodunk, hogy a vizsgálatok a lehető legkevesebb fennakadással járjanak.</p>
|
||||
</article>
|
||||
<article class="landing-feature-card">
|
||||
<h3>Teljes körű dokumentáció</h3>
|
||||
<p>Átlátható, hatósági előírásoknak megfelelő jegyzőkönyveket és jól követhető szakmai összefoglalókat adunk át.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="section-heading">
|
||||
<h2>Miben állunk rendelkezésére?</h2>
|
||||
<p>Röviden összefoglaltuk, hogyan tudjuk támogatni az üzemeltetőket és közös képviselőket.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="landing-service-list">
|
||||
<article class="landing-service-item">
|
||||
<h3>Törvényi előírás szerinti időszakos ellenőrzések</h3>
|
||||
<p>Negyedéves, féléves és éves biztonságtechnikai felülvizsgálatok a jogszabályi megfelelés érdekében.</p>
|
||||
</article>
|
||||
<article class="landing-service-item">
|
||||
<h3>Üzembe helyezés előtti vizsgálat</h3>
|
||||
<p>Új vagy modernizált liftek hatósági átadásának szakértői előkészítése és támogatása.</p>
|
||||
</article>
|
||||
<article class="landing-service-item">
|
||||
<h3>Kockázatértékelés és állapotfelmérés</h3>
|
||||
<p>Részletes műszaki helyzetkép, javítási javaslatokkal és korszerűsítési irányokkal.</p>
|
||||
</article>
|
||||
<article class="landing-service-item">
|
||||
<h3>Szakértői tanácsadás</h3>
|
||||
<p>Segítség közös képviselőknek, üzemeltetőknek és cégvezetőknek a vonatkozó szabványok és követelmények értelmezésében.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="landing-cta panel">
|
||||
<div class="section-heading">
|
||||
<h2>Közeleg a kötelező felülvizsgálat?</h2>
|
||||
<p>
|
||||
A biztonságos liftüzemeltetés és a szabályozások betartása komoly felelősség. Ne hagyja az utolsó
|
||||
pillanatra a vizsgálat megszervezését.
|
||||
</p>
|
||||
</div>
|
||||
<p class="landing-cta-text">
|
||||
A Csödényi Felvonó Felülvizsgáló gyorsan, pontosan és a hatályos előírások szigorú követésével dolgozik,
|
||||
hogy a működés zavartalan, a dokumentáció pedig kifogástalan legyen.
|
||||
</p>
|
||||
<div class="landing-contact-row">
|
||||
<span>Telefon: +36 30 123 4567</span>
|
||||
<span>E-mail: [email protected]</span>
|
||||
<span>Webalkalmazás: technikusok részére</span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Bejelentkezés | LiftEllenőr</title>
|
||||
<link rel="stylesheet" href="/style.css?v=20260522-3" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="pageShell">
|
||||
<header id="siteHeader" class="panel">
|
||||
<a class="brand-block brand-link" href="/">
|
||||
<img class="brand-logo" src="/logo.png" alt="Csödényi Felvonó Felülvizsgáló logó" />
|
||||
<div class="brand-copy">
|
||||
<p class="brand-name">LiftEllenőr</p>
|
||||
<p class="brand-subtitle">Technikusi hozzáférés és vizsgálati naplózás.</p>
|
||||
</div>
|
||||
</a>
|
||||
<nav class="top-nav" aria-label="Fő navigáció">
|
||||
<a class="nav-link" href="/">Főoldal</a>
|
||||
<a class="nav-link nav-link-active" href="/bejelentkezes">Bejelentkezés</a>
|
||||
<a class="nav-link" href="/regisztracio">Regisztráció</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="mainLayout">
|
||||
<section class="panel auth-panel">
|
||||
<div class="section-heading">
|
||||
<h1 class="page-title">Bejelentkezés</h1>
|
||||
<p class="page-text">A rendszer használatához jelentkezz be a felhasználói adataiddal.</p>
|
||||
</div>
|
||||
|
||||
<form id="authForm" class="auth-form">
|
||||
<label for="username">Felhasználónév</label>
|
||||
<input id="username" name="username" type="text" autocomplete="username" required />
|
||||
|
||||
<label for="password">Jelszó</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required />
|
||||
|
||||
<button class="primary-button" type="submit">Bejelentkezés</button>
|
||||
</form>
|
||||
|
||||
<p id="authMessage" class="feedback-text"></p>
|
||||
<p class="auth-link-row">Nincs még hozzáférésed? <a href="/regisztracio">Regisztráció</a></p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/auth.js?v=20260522-1"></script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 836 KiB |
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Napló | LiftEllenőr</title>
|
||||
<link rel="stylesheet" href="/style.css?v=20260522-5" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="pageShell">
|
||||
<main id="appShell">
|
||||
<div id="topBar" class="panel app-topbar">
|
||||
<div class="topbar-heading">
|
||||
<img class="topbar-logo" src="/logo.png" alt="" aria-hidden="true" />
|
||||
<div>
|
||||
<h1 id="welcomeText" class="page-title">Napló</h1>
|
||||
<p id="routeText" class="page-text"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<a class="ghost-button" href="/alkalmazas">Vissza az alkalmazáshoz</a>
|
||||
<button id="logoutButton" class="ghost-button" type="button">Kijelentkezés</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="section-heading">
|
||||
<h2>Bejegyzések</h2>
|
||||
<p>Kereshető és rendezhető ellenőrzési napló a teljes útvonalhoz.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="entryControls">
|
||||
<div class="field-wrap">
|
||||
<label for="searchInput">Keresés</label>
|
||||
<input id="searchInput" type="text" placeholder="Lift, helyszín, gyári szám, technikus" />
|
||||
</div>
|
||||
|
||||
<div class="field-wrap">
|
||||
<label for="sortSelect">Rendezés</label>
|
||||
<select id="sortSelect">
|
||||
<option value="inspectionDateDesc">Ellenőrzés dátuma szerint csökkenő</option>
|
||||
<option value="inspectionDateAsc">Ellenőrzés dátuma szerint növekvő</option>
|
||||
<option value="nextInspectionAsc">Következő ellenőrzés szerint növekvő</option>
|
||||
<option value="nextInspectionDesc">Következő ellenőrzés szerint csökkenő</option>
|
||||
<option value="elevatorNameAsc">Lift neve szerint</option>
|
||||
<option value="statusAsc">Állapot szerint</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="entriesList" class="entries-grid"></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/logs.js?v=20260522-5"></script>
|
||||
</body>
|
||||
</html>
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
const state = {
|
||||
user: null,
|
||||
logs: [],
|
||||
logsLoaded: false,
|
||||
initialLogRetryCount: 0,
|
||||
searchActivated: false
|
||||
};
|
||||
|
||||
const elements = {
|
||||
welcomeText: document.getElementById("welcomeText"),
|
||||
routeText: document.getElementById("routeText"),
|
||||
logoutButton: document.getElementById("logoutButton"),
|
||||
entriesList: document.getElementById("entriesList"),
|
||||
searchInput: document.getElementById("searchInput"),
|
||||
sortSelect: document.getElementById("sortSelect")
|
||||
};
|
||||
|
||||
bindEvents();
|
||||
bootPage();
|
||||
|
||||
function bindEvents() {
|
||||
elements.logoutButton.addEventListener("click", logout);
|
||||
elements.searchInput.addEventListener("input", handleSearchInput);
|
||||
elements.sortSelect.addEventListener("change", loadLogs);
|
||||
window.addEventListener("pageshow", handlePageShow);
|
||||
}
|
||||
|
||||
async function bootPage() {
|
||||
state.logsLoaded = false;
|
||||
state.initialLogRetryCount = 0;
|
||||
state.searchActivated = false;
|
||||
elements.searchInput.value = "";
|
||||
renderLogs();
|
||||
|
||||
try {
|
||||
const response = await apiFetch("/api/me");
|
||||
state.user = response.user;
|
||||
fillUserHeader();
|
||||
await loadLogs(true);
|
||||
} catch (error) {
|
||||
if (error.status === 401) {
|
||||
window.location.href = "/bejelentkezes";
|
||||
return;
|
||||
}
|
||||
|
||||
elements.welcomeText.textContent = "Betöltési hiba";
|
||||
elements.routeText.textContent = "A napló nem tölthető be. Frissítsd az oldalt vagy jelentkezz be újra.";
|
||||
renderError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function fillUserHeader() {
|
||||
const name = state.user?.fullName || "Felhasználó";
|
||||
const route = state.user?.routeName || "Nincs megadva";
|
||||
elements.welcomeText.textContent = "Napló";
|
||||
elements.routeText.textContent = `${name} · Technikusi útvonal: ${route}`;
|
||||
}
|
||||
|
||||
async function loadLogs(isInitialLoad = false) {
|
||||
try {
|
||||
const sortBy = elements.sortSelect.value || "inspectionDateDesc";
|
||||
const response = await apiFetch(`/api/logs?sortBy=${encodeURIComponent(sortBy)}`);
|
||||
|
||||
state.logs = Array.isArray(response.logs)
|
||||
? response.logs.map((log) => ({
|
||||
...log,
|
||||
files: Array.isArray(log.files) ? log.files : []
|
||||
}))
|
||||
: [];
|
||||
|
||||
state.logsLoaded = true;
|
||||
renderLogs();
|
||||
|
||||
if (isInitialLoad && !state.logs.length) {
|
||||
scheduleInitialLogRetry();
|
||||
}
|
||||
} catch (error) {
|
||||
state.logs = [];
|
||||
state.logsLoaded = true;
|
||||
renderError(error.message);
|
||||
|
||||
if (isInitialLoad) {
|
||||
scheduleInitialLogRetry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderLogs() {
|
||||
if (!state.logsLoaded) {
|
||||
elements.entriesList.innerHTML = `
|
||||
<article class="entry-card empty-card">
|
||||
<h3>Betöltés</h3>
|
||||
<p>A bejegyzések betöltése folyamatban van.</p>
|
||||
</article>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const query = state.searchActivated ? elements.searchInput.value.trim().toLowerCase() : "";
|
||||
const visibleLogs = state.logs.filter((log) => searchableLog(log).includes(query));
|
||||
|
||||
if (!visibleLogs.length) {
|
||||
elements.entriesList.innerHTML = `
|
||||
<article class="entry-card empty-card">
|
||||
<h3>Nincs találat</h3>
|
||||
<p>Módosítsd a keresést, vagy hozz létre új ellenőrzési bejegyzést az alkalmazás oldalon.</p>
|
||||
</article>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
elements.entriesList.innerHTML = visibleLogs
|
||||
.map((log) => {
|
||||
const fileMarkup = log.files.length
|
||||
? log.files
|
||||
.map(
|
||||
(file) => `
|
||||
<a class="file-link" href="${file.downloadPath}">
|
||||
${escapeHtml(file.originalName)}
|
||||
</a>
|
||||
`
|
||||
)
|
||||
.join("")
|
||||
: `<span class="muted-text">Nincs PDF csatolmány</span>`;
|
||||
|
||||
return `
|
||||
<article class="entry-card">
|
||||
<div class="card-topline">
|
||||
<span class="status-chip ${statusClassName(log.status)}">${humanizeStatus(log.status)}</span>
|
||||
<span class="muted-text">${formatDate(log.inspectionDate)}</span>
|
||||
</div>
|
||||
<h3>${escapeHtml(log.elevatorName)}</h3>
|
||||
<p><strong>Gyári szám:</strong> ${escapeHtml(log.serialNumber)}</p>
|
||||
<p><strong>Helyszín:</strong> ${escapeHtml(log.location)}</p>
|
||||
<p><strong>Útvonal:</strong> ${escapeHtml(log.routeName)}</p>
|
||||
<p><strong>Technikus:</strong> ${escapeHtml(log.technicianName)}</p>
|
||||
<p><strong>Következő ellenőrzés:</strong> ${formatDate(log.nextInspectionDate)}</p>
|
||||
<p><strong>Megjegyzés:</strong> ${escapeHtml(log.notes || "Nincs megjegyzés.")}</p>
|
||||
<div class="file-list">${fileMarkup}</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderError(message) {
|
||||
elements.entriesList.innerHTML = `
|
||||
<article class="entry-card empty-card">
|
||||
<h3>Hiba</h3>
|
||||
<p>${escapeHtml(message)}</p>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function scheduleInitialLogRetry() {
|
||||
if (state.initialLogRetryCount >= 3 || state.logs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.initialLogRetryCount += 1;
|
||||
|
||||
window.setTimeout(() => {
|
||||
state.logsLoaded = false;
|
||||
renderLogs();
|
||||
loadLogs(false);
|
||||
}, 500 * state.initialLogRetryCount);
|
||||
}
|
||||
|
||||
function handleSearchInput() {
|
||||
state.searchActivated = true;
|
||||
renderLogs();
|
||||
}
|
||||
|
||||
function handlePageShow() {
|
||||
if (state.user) {
|
||||
state.logsLoaded = false;
|
||||
renderLogs();
|
||||
loadLogs();
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
const response = await apiFetch("/api/logout", { method: "POST" });
|
||||
window.location.href = response.redirectPath;
|
||||
} catch (_error) {
|
||||
window.location.href = "/bejelentkezes";
|
||||
}
|
||||
}
|
||||
|
||||
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 searchableLog(log) {
|
||||
return [
|
||||
log.elevatorName,
|
||||
log.serialNumber,
|
||||
log.location,
|
||||
log.routeName,
|
||||
log.technicianName,
|
||||
log.status,
|
||||
log.notes
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
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 escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Regisztráció | LiftEllenőr</title>
|
||||
<link rel="stylesheet" href="/style.css?v=20260522-3" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="pageShell">
|
||||
<header id="siteHeader" class="panel">
|
||||
<a class="brand-block brand-link" href="/">
|
||||
<img class="brand-logo" src="/logo.png" alt="Csödényi Felvonó Felülvizsgáló logó" />
|
||||
<div class="brand-copy">
|
||||
<p class="brand-name">LiftEllenőr</p>
|
||||
<p class="brand-subtitle">Technikusi hozzáférés és vizsgálati naplózás.</p>
|
||||
</div>
|
||||
</a>
|
||||
<nav class="top-nav" aria-label="Fő navigáció">
|
||||
<a class="nav-link" href="/">Főoldal</a>
|
||||
<a class="nav-link" href="/bejelentkezes">Bejelentkezés</a>
|
||||
<a class="nav-link nav-link-active" href="/regisztracio">Regisztráció</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="mainLayout">
|
||||
<section class="panel auth-panel">
|
||||
<div class="section-heading">
|
||||
<h1 class="page-title">Regisztráció</h1>
|
||||
<p class="page-text">Új technikusi hozzáférés létrehozása.</p>
|
||||
</div>
|
||||
|
||||
<form id="authForm" class="auth-form">
|
||||
<label for="fullName">Teljes név</label>
|
||||
<input id="fullName" name="fullName" type="text" required />
|
||||
|
||||
<label for="routeName">Technikusi útvonal</label>
|
||||
<input id="routeName" name="routeName" type="text" placeholder="Például: Dél-Pest" required />
|
||||
|
||||
<label for="username">Felhasználónév</label>
|
||||
<input id="username" name="username" type="text" autocomplete="username" required />
|
||||
|
||||
<label for="password">Jelszó</label>
|
||||
<input id="password" name="password" type="password" autocomplete="new-password" required />
|
||||
|
||||
<button class="secondary-button" type="submit">Regisztráció mentése</button>
|
||||
</form>
|
||||
|
||||
<p id="authMessage" class="feedback-text"></p>
|
||||
<p class="auth-link-row">Van már fiókod? <a href="/bejelentkezes">Bejelentkezés</a></p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/auth.js?v=20260522-1"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,788 @@
|
||||
:root {
|
||||
--bg: #f3f4f6;
|
||||
--panel: #ffffff;
|
||||
--line: #d9dee7;
|
||||
--line-strong: #c8d0dc;
|
||||
--text: #1f2937;
|
||||
--muted: #6b7280;
|
||||
--accent: #2563eb;
|
||||
--accent-dark: #1d4ed8;
|
||||
--warning: #b45309;
|
||||
--danger: #b91c1c;
|
||||
--success: #15803d;
|
||||
--shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Arial, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(37, 99, 235, 0.08), transparent 28%),
|
||||
linear-gradient(180deg, #f7faff, var(--bg) 18%);
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 0.85rem;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#pageShell {
|
||||
width: min(1180px, calc(100% - 2rem));
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 0 2.5rem;
|
||||
}
|
||||
|
||||
#heroText,
|
||||
.panel-head p,
|
||||
#routeText,
|
||||
.muted-text,
|
||||
.feedback-text,
|
||||
.entry-card p,
|
||||
.dashboard-card p,
|
||||
.auth-link-row {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#mainLayout,
|
||||
#appShell {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.landing-layout {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.landing-header {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
z-index: 2;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 252, 0.98)),
|
||||
var(--panel);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
#siteHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-color: #d6e2f7;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(239, 245, 255, 0.96), rgba(255, 255, 255, 0.98)),
|
||||
var(--panel);
|
||||
}
|
||||
|
||||
.brand-block {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
object-fit: contain;
|
||||
border-radius: 14px;
|
||||
flex: 0 0 auto;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 22px rgba(37, 99, 235, 0.14);
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
margin: 0.15rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.landing-header .brand-block {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.landing-header .brand-copy {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.landing-header .brand-logo {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
padding: 0.2rem;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.landing-header .brand-name {
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 0.55rem 0.8rem;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.nav-link-active {
|
||||
border-color: var(--line-strong);
|
||||
background: #eef4ff;
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.landing-kicker {
|
||||
margin: 0 0 0.6rem;
|
||||
color: var(--accent-dark);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.landing-title {
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 4vw, 3.4rem);
|
||||
line-height: 1.05;
|
||||
max-width: 12ch;
|
||||
}
|
||||
|
||||
.landing-lead {
|
||||
margin: 0;
|
||||
font-size: 1.02rem;
|
||||
color: var(--muted);
|
||||
max-width: 68ch;
|
||||
}
|
||||
|
||||
.page-text {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.panel-actions,
|
||||
.inline-actions,
|
||||
.device-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.panel-head h2,
|
||||
.entry-card h3,
|
||||
.dashboard-card h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.panel-head h2 {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.tab-button,
|
||||
.ghost-button,
|
||||
.primary-button,
|
||||
.secondary-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
padding: 0.7rem 1rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.tab-button,
|
||||
.ghost-button {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.primary-button:hover {
|
||||
background: var(--accent-dark);
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
background: #eef4ff;
|
||||
color: var(--accent-dark);
|
||||
border: 1px solid #cfe0ff;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background: #e3edff;
|
||||
}
|
||||
|
||||
.ghost-button:hover,
|
||||
.tab-button:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.app-topbar .ghost-button {
|
||||
border-color: #bfd3fb;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.app-topbar .ghost-button:hover {
|
||||
background: #eff5ff;
|
||||
}
|
||||
|
||||
.compact-button {
|
||||
padding: 0.55rem 0.8rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.auth-form,
|
||||
#logForm {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
#deviceForm {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#topBar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-topbar {
|
||||
border-color: #cfe0ff;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(37, 99, 235, 0.12), rgba(255, 255, 255, 0.96) 28%),
|
||||
var(--panel);
|
||||
}
|
||||
|
||||
.topbar-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar-logo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: contain;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 8px 18px rgba(37, 99, 235, 0.12);
|
||||
padding: 0.2rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.landing-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.8fr) minmax(240px, 0.8fr);
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
padding: 1.5rem;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(37, 99, 235, 0.1), transparent 32%),
|
||||
linear-gradient(180deg, #ffffff, #f7f9fc);
|
||||
}
|
||||
|
||||
.landing-hero-copy {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.landing-hero-logo {
|
||||
width: min(100%, 320px);
|
||||
max-height: 320px;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
object-fit: contain;
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 18px 36px rgba(37, 99, 235, 0.16);
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.hero-sidecard {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.9rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #dbe4f0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, #eef4ff, #ffffff);
|
||||
}
|
||||
|
||||
.hero-sidecard-label {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
|
||||
.hero-sidecard-text {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.landing-login-link {
|
||||
padding: 0.45rem 0.75rem;
|
||||
font-size: 0.92rem;
|
||||
border-color: #bfd3fb;
|
||||
color: var(--accent-dark);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.landing-feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.landing-feature-card,
|
||||
.landing-service-item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #ffffff, #f8fafc);
|
||||
}
|
||||
|
||||
.landing-feature-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.landing-feature-card h3,
|
||||
.landing-service-item h3 {
|
||||
margin: 0 0 0.55rem;
|
||||
}
|
||||
|
||||
.landing-feature-card p,
|
||||
.landing-service-item p,
|
||||
.landing-cta-text {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.landing-service-list {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.landing-service-item {
|
||||
padding: 1rem 1rem 1rem 1.15rem;
|
||||
border-left: 4px solid #cfe0ff;
|
||||
}
|
||||
|
||||
.landing-cta {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(37, 99, 235, 0.08), rgba(255, 255, 255, 1) 38%),
|
||||
#fff;
|
||||
}
|
||||
|
||||
.landing-contact-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1.25rem;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-grid,
|
||||
.entries-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-card,
|
||||
.entry-card {
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.card-topline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.8rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-chip,
|
||||
.serial-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
padding: 0.3rem 0.65rem;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.serial-chip {
|
||||
background: #f3f4f6;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.success-chip {
|
||||
background: #eaf7ef;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.warning-chip {
|
||||
background: #fff4e5;
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.danger-chip {
|
||||
background: #fdecec;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.countdown-box {
|
||||
margin: 1rem 0 0.75rem;
|
||||
padding: 0.85rem 0.95rem;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--line);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-grid,
|
||||
#entryControls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#entryControls {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.field-wrap {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.readonly-input {
|
||||
background: #f8fafc;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.field-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.feedback-text {
|
||||
min-height: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.feedback-success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.feedback-error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
padding: 0.95rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: #f8fafc;
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 1rem;
|
||||
background: rgba(15, 23, 42, 0.52);
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
width: min(760px, 100%);
|
||||
max-height: calc(100vh - 2rem);
|
||||
overflow: auto;
|
||||
border: 1px solid #d6e2f7;
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 252, 0.98)),
|
||||
#fff;
|
||||
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.24);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.file-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
background: #f8fafc;
|
||||
color: var(--accent-dark);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.file-link:hover {
|
||||
background: #eef4ff;
|
||||
border-color: #cfe0ff;
|
||||
}
|
||||
|
||||
.auth-link-row {
|
||||
margin: 0.4rem 0 0;
|
||||
}
|
||||
|
||||
.auth-link-row a {
|
||||
color: var(--accent-dark);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-link-row a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.landing-hero,
|
||||
.landing-feature-grid,
|
||||
.dashboard-grid,
|
||||
.entries-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
#pageShell {
|
||||
width: min(100% - 1rem, 1180px);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
#siteHeader,
|
||||
#topBar,
|
||||
.panel-head,
|
||||
.form-actions,
|
||||
.modal-head {
|
||||
display: grid;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.brand-block,
|
||||
.topbar-heading {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.landing-hero,
|
||||
.landing-feature-grid,
|
||||
.form-grid,
|
||||
#entryControls,
|
||||
.dashboard-grid,
|
||||
.entries-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#mainTitle {
|
||||
max-width: none;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.landing-title {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.landing-hero-logo {
|
||||
width: min(100%, 220px);
|
||||
max-height: 220px;
|
||||
}
|
||||
|
||||
.brand-logo,
|
||||
.topbar-logo {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user