Lisää saatavuuskyselyjen keräys ja listausnäkymä
Jokainen nettisivujen kautta tehty saatavuustarkistus tallennetaan tietokantaan (osoite, postinumero, kaupunki, tulos, IP, referer). Kyselyt näkyvät Asiakkaat > Saatavuuskyselyt -välilehdellä sivutettuna taulukkona. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
38
api.php
38
api.php
@@ -1215,12 +1215,48 @@ switch ($action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tallenna kysely tietokantaan
|
||||||
|
try {
|
||||||
|
_dbExecute(
|
||||||
|
"INSERT INTO availability_queries (company_id, osoite, postinumero, kaupunki, saatavilla, ip_address, user_agent, referer, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
[
|
||||||
|
$matchedCompany['id'],
|
||||||
|
$_GET['osoite'] ?? '',
|
||||||
|
$_GET['postinumero'] ?? '',
|
||||||
|
$_GET['kaupunki'] ?? '',
|
||||||
|
$found ? 1 : 0,
|
||||||
|
getClientIp(),
|
||||||
|
substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 500),
|
||||||
|
substr($_SERVER['HTTP_REFERER'] ?? '', 0, 500),
|
||||||
|
date('Y-m-d H:i:s'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) { /* logitus ei saa kaataa API-vastausta */ }
|
||||||
|
|
||||||
echo json_encode(['saatavilla' => $found]);
|
echo json_encode(['saatavilla' => $found]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ---------- SAATAVUUSKYSELYT ----------
|
||||||
|
case 'availability_queries':
|
||||||
|
requireAuth();
|
||||||
|
$companyId = requireCompanyOrParam();
|
||||||
|
$limit = (int)($_GET['limit'] ?? 100);
|
||||||
|
$offset = (int)($_GET['offset'] ?? 0);
|
||||||
|
if ($limit > 500) $limit = 500;
|
||||||
|
|
||||||
|
$total = (int)_dbFetchScalar("SELECT COUNT(*) FROM availability_queries WHERE company_id = ?", [$companyId]);
|
||||||
|
$rows = _dbFetchAll(
|
||||||
|
"SELECT id, osoite, postinumero, kaupunki, saatavilla, ip_address, referer, created_at
|
||||||
|
FROM availability_queries WHERE company_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
||||||
|
[$companyId, $limit, $offset]
|
||||||
|
);
|
||||||
|
echo json_encode(['total' => $total, 'queries' => $rows]);
|
||||||
|
break;
|
||||||
|
|
||||||
// ---------- CONFIG (admin, yrityskohtainen) ----------
|
// ---------- CONFIG (admin, yrityskohtainen) ----------
|
||||||
case 'config':
|
case 'config':
|
||||||
requireAdmin();
|
requireAuth();
|
||||||
$companyId = requireCompany();
|
$companyId = requireCompany();
|
||||||
$globalConf = dbLoadConfig();
|
$globalConf = dbLoadConfig();
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
|
|||||||
17
db.php
17
db.php
@@ -614,6 +614,23 @@ function initDatabase(): void {
|
|||||||
UNIQUE KEY uk_company_type (company_id, type),
|
UNIQUE KEY uk_company_type (company_id, type),
|
||||||
INDEX idx_company (company_id)
|
INDEX idx_company (company_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
|
||||||
|
|
||||||
|
"CREATE TABLE IF NOT EXISTS availability_queries (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
company_id VARCHAR(50) NOT NULL,
|
||||||
|
osoite VARCHAR(255) NOT NULL,
|
||||||
|
postinumero VARCHAR(20) NOT NULL,
|
||||||
|
kaupunki VARCHAR(100) NOT NULL,
|
||||||
|
saatavilla BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
ip_address VARCHAR(45) DEFAULT '',
|
||||||
|
user_agent VARCHAR(500) DEFAULT '',
|
||||||
|
referer VARCHAR(500) DEFAULT '',
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
INDEX idx_company (company_id),
|
||||||
|
INDEX idx_created (created_at),
|
||||||
|
INDEX idx_postinumero (postinumero),
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($tables as $i => $sql) {
|
foreach ($tables as $i => $sql) {
|
||||||
|
|||||||
33
index.html
33
index.html
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Noxus HUB</title>
|
<title>Noxus HUB</title>
|
||||||
<link rel="stylesheet" href="style.css?v=20260313i">
|
<link rel="stylesheet" href="style.css?v=20260313j">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Login -->
|
<!-- Login -->
|
||||||
@@ -93,6 +93,7 @@
|
|||||||
<div class="sub-tab-bar" id="customers-sub-tab-bar">
|
<div class="sub-tab-bar" id="customers-sub-tab-bar">
|
||||||
<button class="sub-tab active" data-cust-subtab="customers-list">Asiakkaat</button>
|
<button class="sub-tab active" data-cust-subtab="customers-list">Asiakkaat</button>
|
||||||
<button class="sub-tab" data-cust-subtab="customers-archive">Arkisto</button>
|
<button class="sub-tab" data-cust-subtab="customers-archive">Arkisto</button>
|
||||||
|
<button class="sub-tab" data-cust-subtab="customers-availability">Saatavuuskyselyt</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="subtab-customers-list" class="sub-tab-content active">
|
<div id="subtab-customers-list" class="sub-tab-content active">
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
@@ -189,6 +190,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /subtab-customers-archive -->
|
</div><!-- /subtab-customers-archive -->
|
||||||
|
|
||||||
|
<div id="subtab-customers-availability" class="sub-tab-content">
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="table-card" style="padding:1.5rem;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin:0;color:#0f3460;">Saatavuuskyselyt</h3>
|
||||||
|
<p style="color:#888;font-size:0.85rem;margin:0.25rem 0 0;">Nettisivujen kautta tehdyt saatavuustarkistukset</p>
|
||||||
|
</div>
|
||||||
|
<span id="availability-query-count" style="color:#888;font-size:0.85rem;"></span>
|
||||||
|
</div>
|
||||||
|
<table id="availability-queries-table" class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Aika</th>
|
||||||
|
<th>Osoite</th>
|
||||||
|
<th>Postinumero</th>
|
||||||
|
<th>Kaupunki</th>
|
||||||
|
<th>Tulos</th>
|
||||||
|
<th>Lähde</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="availability-pagination" style="margin-top:1rem;display:flex;justify-content:center;gap:0.5rem;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /subtab-customers-availability -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab: Liidit -->
|
<!-- Tab: Liidit -->
|
||||||
@@ -2233,6 +2262,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="script.js?v=20260313i"></script>
|
<script src="script.js?v=20260313j"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
58
script.js
58
script.js
@@ -281,6 +281,8 @@ function switchToTab(target, subTab) {
|
|||||||
loadCustomers();
|
loadCustomers();
|
||||||
if (subTab === 'archive') {
|
if (subTab === 'archive') {
|
||||||
switchCustomerSubTab('customers-archive');
|
switchCustomerSubTab('customers-archive');
|
||||||
|
} else if (subTab === 'availability') {
|
||||||
|
switchCustomerSubTab('customers-availability');
|
||||||
} else {
|
} else {
|
||||||
switchCustomerSubTab('customers-list');
|
switchCustomerSubTab('customers-list');
|
||||||
}
|
}
|
||||||
@@ -3495,6 +3497,7 @@ function switchCustomerSubTab(target) {
|
|||||||
const content = document.getElementById('subtab-' + target);
|
const content = document.getElementById('subtab-' + target);
|
||||||
if (content) content.classList.add('active');
|
if (content) content.classList.add('active');
|
||||||
if (target === 'customers-archive') { loadArchive(); window.location.hash = 'customers/archive'; }
|
if (target === 'customers-archive') { loadArchive(); window.location.hash = 'customers/archive'; }
|
||||||
|
else if (target === 'customers-availability') { loadAvailabilityQueries(); window.location.hash = 'customers/availability'; }
|
||||||
else { window.location.hash = 'customers'; }
|
else { window.location.hash = 'customers'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3502,6 +3505,61 @@ document.querySelectorAll('#customers-sub-tab-bar .sub-tab').forEach(btn => {
|
|||||||
btn.addEventListener('click', () => switchCustomerSubTab(btn.dataset.custSubtab));
|
btn.addEventListener('click', () => switchCustomerSubTab(btn.dataset.custSubtab));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== SAATAVUUSKYSELYT ====================
|
||||||
|
|
||||||
|
let availabilityPage = 0;
|
||||||
|
const AVAILABILITY_PER_PAGE = 50;
|
||||||
|
|
||||||
|
async function loadAvailabilityQueries(page = 0) {
|
||||||
|
availabilityPage = page;
|
||||||
|
const offset = page * AVAILABILITY_PER_PAGE;
|
||||||
|
try {
|
||||||
|
const data = await apiCall(`availability_queries&limit=${AVAILABILITY_PER_PAGE}&offset=${offset}`);
|
||||||
|
const tbody = document.querySelector('#availability-queries-table tbody');
|
||||||
|
const countEl = document.getElementById('availability-query-count');
|
||||||
|
countEl.textContent = `Yhteensä ${data.total} kyselyä`;
|
||||||
|
|
||||||
|
if (data.queries.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#888;padding:2rem;">Ei vielä kyselyjä</td></tr>';
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = data.queries.map(q => {
|
||||||
|
const date = q.created_at ? q.created_at.replace('T', ' ').substring(0, 16) : '';
|
||||||
|
const found = q.saatavilla == 1;
|
||||||
|
const badge = found
|
||||||
|
? '<span style="background:#e8f5e9;color:#2e7d32;padding:2px 8px;border-radius:10px;font-size:0.8rem;">Saatavilla</span>'
|
||||||
|
: '<span style="background:#fce4ec;color:#c62828;padding:2px 8px;border-radius:10px;font-size:0.8rem;">Ei saatavilla</span>';
|
||||||
|
let source = '';
|
||||||
|
if (q.referer) {
|
||||||
|
try { source = new URL(q.referer).hostname; } catch(e) { source = q.referer.substring(0, 30); }
|
||||||
|
}
|
||||||
|
return `<tr>
|
||||||
|
<td style="white-space:nowrap;">${esc(date)}</td>
|
||||||
|
<td>${esc(q.osoite)}</td>
|
||||||
|
<td>${esc(q.postinumero)}</td>
|
||||||
|
<td>${esc(q.kaupunki)}</td>
|
||||||
|
<td>${badge}</td>
|
||||||
|
<td style="font-size:0.8rem;color:#888;">${esc(source)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sivutus
|
||||||
|
const totalPages = Math.ceil(data.total / AVAILABILITY_PER_PAGE);
|
||||||
|
const pagination = document.getElementById('availability-pagination');
|
||||||
|
if (totalPages > 1) {
|
||||||
|
let pagHtml = '';
|
||||||
|
if (page > 0) pagHtml += `<button class="btn-secondary" onclick="loadAvailabilityQueries(${page - 1})">← Edellinen</button>`;
|
||||||
|
pagHtml += `<span style="padding:6px 12px;color:#666;">Sivu ${page + 1} / ${totalPages}</span>`;
|
||||||
|
if (page < totalPages - 1) pagHtml += `<button class="btn-secondary" onclick="loadAvailabilityQueries(${page + 1})">Seuraava →</button>`;
|
||||||
|
pagination.innerHTML = pagHtml;
|
||||||
|
} else {
|
||||||
|
pagination.innerHTML = '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Saatavuuskyselyjen lataus epäonnistui:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== SIJAINNIT — YHDISTETTY LAITETILOIHIN ====================
|
// ==================== SIJAINNIT — YHDISTETTY LAITETILOIHIN ====================
|
||||||
// Sites-koodi poistettu: sijainnit hallitaan nyt Laitetilat-välilehdellä.
|
// Sites-koodi poistettu: sijainnit hallitaan nyt Laitetilat-välilehdellä.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user