feat: Laitteet-moduuli (inventaario) + sijaintien hallinta + login-fix
- Uusi "Laitteet" välilehti navigaatiossa (devices-moduuli) - Taulukko: Nimi, Hallintaosoite, Serial, Sijainti, Funktio, Tyyppi, Malli, Ping - Lisää/muokkaa/poista laitteita modaali-lomakkeella - Hakupalkki suodattaa kaikista kentistä - Ping-check täppä valmiina tulevaa toteutusta varten - Sijainnit (Sites) -hallinta yrityksen asetuksissa - Lisää/muokkaa/poista sijainteja (toimipisteet, konesalit) - Sijainnit näkyvät laitelomakkeen dropdownissa - Laitteet-moduuli lisätty moduulijärjestelmään (checkbox yritysasetuksissa) - DB: sites + devices taulut, CRUD-funktiot - Fix: Login-näkymä ei enää vilku refreshissä (piilotettu oletuksena) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
95
api.php
95
api.php
@@ -1309,6 +1309,101 @@ switch ($action) {
|
|||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ---------- SIJAINNIT (SITES) ----------
|
||||||
|
case 'sites':
|
||||||
|
requireAuth();
|
||||||
|
$companyId = requireCompany();
|
||||||
|
echo json_encode(dbLoadSites($companyId));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'site_save':
|
||||||
|
requireAdmin();
|
||||||
|
$companyId = requireCompany();
|
||||||
|
if ($method !== 'POST') break;
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$site = [
|
||||||
|
'id' => $input['id'] ?? generateId(),
|
||||||
|
'nimi' => trim($input['nimi'] ?? ''),
|
||||||
|
'osoite' => trim($input['osoite'] ?? ''),
|
||||||
|
'kaupunki' => trim($input['kaupunki'] ?? ''),
|
||||||
|
];
|
||||||
|
if (empty($site['nimi'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Sijainnin nimi vaaditaan']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dbSaveSite($companyId, $site);
|
||||||
|
dbAddLog($companyId, currentUser(), 'site_save', $site['id'], $site['nimi'], (isset($input['id']) ? 'Muokkasi' : 'Lisäsi') . ' sijainnin');
|
||||||
|
echo json_encode($site);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'site_delete':
|
||||||
|
requireAdmin();
|
||||||
|
$companyId = requireCompany();
|
||||||
|
if ($method !== 'POST') break;
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $input['id'] ?? '';
|
||||||
|
// Hae nimi logitusta varten
|
||||||
|
$sites = dbLoadSites($companyId);
|
||||||
|
$siteName = '';
|
||||||
|
foreach ($sites as $s) { if ($s['id'] === $id) { $siteName = $s['nimi']; break; } }
|
||||||
|
dbDeleteSite($id);
|
||||||
|
dbAddLog($companyId, currentUser(), 'site_delete', $id, $siteName, 'Poisti sijainnin');
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ---------- LAITTEET (DEVICES) ----------
|
||||||
|
case 'devices':
|
||||||
|
requireAuth();
|
||||||
|
$companyId = requireCompany();
|
||||||
|
echo json_encode(dbLoadDevices($companyId));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'device_save':
|
||||||
|
requireAuth();
|
||||||
|
$companyId = requireCompany();
|
||||||
|
if ($method !== 'POST') break;
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$isNew = empty($input['id']);
|
||||||
|
$device = [
|
||||||
|
'id' => $input['id'] ?? generateId(),
|
||||||
|
'nimi' => trim($input['nimi'] ?? ''),
|
||||||
|
'hallintaosoite' => trim($input['hallintaosoite'] ?? ''),
|
||||||
|
'serial' => trim($input['serial'] ?? ''),
|
||||||
|
'site_id' => $input['site_id'] ?? null,
|
||||||
|
'funktio' => trim($input['funktio'] ?? ''),
|
||||||
|
'tyyppi' => trim($input['tyyppi'] ?? ''),
|
||||||
|
'malli' => trim($input['malli'] ?? ''),
|
||||||
|
'ping_check' => !empty($input['ping_check']),
|
||||||
|
'lisatiedot' => trim($input['lisatiedot'] ?? ''),
|
||||||
|
'luotu' => $isNew ? date('Y-m-d H:i:s') : ($input['luotu'] ?? date('Y-m-d H:i:s')),
|
||||||
|
'muokattu' => $isNew ? null : date('Y-m-d H:i:s'),
|
||||||
|
'muokkaaja' => $isNew ? '' : currentUser(),
|
||||||
|
];
|
||||||
|
if (empty($device['nimi'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Laitteen nimi vaaditaan']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dbSaveDevice($companyId, $device);
|
||||||
|
dbAddLog($companyId, currentUser(), $isNew ? 'device_create' : 'device_update', $device['id'], $device['nimi'], ($isNew ? 'Lisäsi' : 'Muokkasi') . ' laitteen');
|
||||||
|
echo json_encode($device);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'device_delete':
|
||||||
|
requireAuth();
|
||||||
|
$companyId = requireCompany();
|
||||||
|
if ($method !== 'POST') break;
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $input['id'] ?? '';
|
||||||
|
$devices = dbLoadDevices($companyId);
|
||||||
|
$deviceName = '';
|
||||||
|
foreach ($devices as $d) { if ($d['id'] === $id) { $deviceName = $d['nimi']; break; } }
|
||||||
|
dbDeleteDevice($id);
|
||||||
|
dbAddLog($companyId, currentUser(), 'device_delete', $id, $deviceName, 'Poisti laitteen');
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
// ---------- ARCHIVE ----------
|
// ---------- ARCHIVE ----------
|
||||||
case 'archived_customers':
|
case 'archived_customers':
|
||||||
requireAuth();
|
requireAuth();
|
||||||
|
|||||||
106
db.php
106
db.php
@@ -350,6 +350,37 @@ function initDatabase(): void {
|
|||||||
INDEX idx_company (company_id)
|
INDEX idx_company (company_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||||
|
|
||||||
|
"CREATE TABLE IF NOT EXISTS sites (
|
||||||
|
id VARCHAR(20) PRIMARY KEY,
|
||||||
|
company_id VARCHAR(50) NOT NULL,
|
||||||
|
nimi VARCHAR(255) NOT NULL,
|
||||||
|
osoite VARCHAR(255) DEFAULT '',
|
||||||
|
kaupunki VARCHAR(100) DEFAULT '',
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_company (company_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||||
|
|
||||||
|
"CREATE TABLE IF NOT EXISTS devices (
|
||||||
|
id VARCHAR(20) PRIMARY KEY,
|
||||||
|
company_id VARCHAR(50) NOT NULL,
|
||||||
|
nimi VARCHAR(255) NOT NULL,
|
||||||
|
hallintaosoite VARCHAR(255) DEFAULT '',
|
||||||
|
serial VARCHAR(255) DEFAULT '',
|
||||||
|
site_id VARCHAR(20) NULL,
|
||||||
|
funktio VARCHAR(255) DEFAULT '',
|
||||||
|
tyyppi VARCHAR(100) DEFAULT '',
|
||||||
|
malli VARCHAR(255) DEFAULT '',
|
||||||
|
ping_check BOOLEAN DEFAULT FALSE,
|
||||||
|
ping_status VARCHAR(20) DEFAULT '',
|
||||||
|
ping_checked_at DATETIME NULL,
|
||||||
|
lisatiedot TEXT,
|
||||||
|
luotu DATETIME,
|
||||||
|
muokattu DATETIME NULL,
|
||||||
|
muokkaaja VARCHAR(100) DEFAULT '',
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_company (company_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||||
|
|
||||||
"CREATE TABLE IF NOT EXISTS files (
|
"CREATE TABLE IF NOT EXISTS files (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
company_id VARCHAR(50) NOT NULL,
|
company_id VARCHAR(50) NOT NULL,
|
||||||
@@ -749,6 +780,81 @@ function dbDeleteCustomer(string $customerId): void {
|
|||||||
_dbExecute("DELETE FROM customers WHERE id = ?", [$customerId]);
|
_dbExecute("DELETE FROM customers WHERE id = ?", [$customerId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== SIJAINNIT (SITES) ====================
|
||||||
|
|
||||||
|
function dbLoadSites(string $companyId): array {
|
||||||
|
return _dbFetchAll("SELECT * FROM sites WHERE company_id = ? ORDER BY nimi", [$companyId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbSaveSite(string $companyId, array $site): void {
|
||||||
|
_dbExecute("
|
||||||
|
INSERT INTO sites (id, company_id, nimi, osoite, kaupunki)
|
||||||
|
VALUES (:id, :company_id, :nimi, :osoite, :kaupunki)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
nimi = VALUES(nimi), osoite = VALUES(osoite), kaupunki = VALUES(kaupunki)
|
||||||
|
", [
|
||||||
|
'id' => $site['id'],
|
||||||
|
'company_id' => $companyId,
|
||||||
|
'nimi' => $site['nimi'] ?? '',
|
||||||
|
'osoite' => $site['osoite'] ?? '',
|
||||||
|
'kaupunki' => $site['kaupunki'] ?? '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbDeleteSite(string $siteId): void {
|
||||||
|
// Nollaa viittaavien laitteiden site_id
|
||||||
|
_dbExecute("UPDATE devices SET site_id = NULL WHERE site_id = ?", [$siteId]);
|
||||||
|
_dbExecute("DELETE FROM sites WHERE id = ?", [$siteId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== LAITTEET (DEVICES) ====================
|
||||||
|
|
||||||
|
function dbLoadDevices(string $companyId): array {
|
||||||
|
$devices = _dbFetchAll("
|
||||||
|
SELECT d.*, s.nimi AS site_name
|
||||||
|
FROM devices d
|
||||||
|
LEFT JOIN sites s ON d.site_id = s.id
|
||||||
|
WHERE d.company_id = ?
|
||||||
|
ORDER BY d.nimi
|
||||||
|
", [$companyId]);
|
||||||
|
foreach ($devices as &$d) {
|
||||||
|
$d['ping_check'] = (bool)$d['ping_check'];
|
||||||
|
unset($d['company_id']);
|
||||||
|
}
|
||||||
|
return $devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbSaveDevice(string $companyId, array $device): void {
|
||||||
|
_dbExecute("
|
||||||
|
INSERT INTO devices (id, company_id, nimi, hallintaosoite, serial, site_id, funktio, tyyppi, malli, ping_check, lisatiedot, luotu, muokattu, muokkaaja)
|
||||||
|
VALUES (:id, :company_id, :nimi, :hallintaosoite, :serial, :site_id, :funktio, :tyyppi, :malli, :ping_check, :lisatiedot, :luotu, :muokattu, :muokkaaja)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
nimi = VALUES(nimi), hallintaosoite = VALUES(hallintaosoite), serial = VALUES(serial),
|
||||||
|
site_id = VALUES(site_id), funktio = VALUES(funktio), tyyppi = VALUES(tyyppi),
|
||||||
|
malli = VALUES(malli), ping_check = VALUES(ping_check), lisatiedot = VALUES(lisatiedot),
|
||||||
|
muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja)
|
||||||
|
", [
|
||||||
|
'id' => $device['id'],
|
||||||
|
'company_id' => $companyId,
|
||||||
|
'nimi' => $device['nimi'] ?? '',
|
||||||
|
'hallintaosoite' => $device['hallintaosoite'] ?? '',
|
||||||
|
'serial' => $device['serial'] ?? '',
|
||||||
|
'site_id' => !empty($device['site_id']) ? $device['site_id'] : null,
|
||||||
|
'funktio' => $device['funktio'] ?? '',
|
||||||
|
'tyyppi' => $device['tyyppi'] ?? '',
|
||||||
|
'malli' => $device['malli'] ?? '',
|
||||||
|
'ping_check' => $device['ping_check'] ?? false,
|
||||||
|
'lisatiedot' => $device['lisatiedot'] ?? '',
|
||||||
|
'luotu' => $device['luotu'] ?? date('Y-m-d H:i:s'),
|
||||||
|
'muokattu' => $device['muokattu'] ?? null,
|
||||||
|
'muokkaaja' => $device['muokkaaja'] ?? '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbDeleteDevice(string $deviceId): void {
|
||||||
|
_dbExecute("DELETE FROM devices WHERE id = ?", [$deviceId]);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== LIIDIT ====================
|
// ==================== LIIDIT ====================
|
||||||
|
|
||||||
function dbLoadLeads(string $companyId): array {
|
function dbLoadLeads(string $companyId): array {
|
||||||
|
|||||||
131
index.html
131
index.html
@@ -8,7 +8,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Login -->
|
<!-- Login -->
|
||||||
<div id="login-screen" class="login-screen">
|
<div id="login-screen" class="login-screen" style="display:none">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<img id="login-logo" src="" alt="Logo" style="height:48px;margin-bottom:0.75rem;display:none;">
|
<img id="login-logo" src="" alt="Logo" style="height:48px;margin-bottom:0.75rem;display:none;">
|
||||||
<h1 id="login-title">Noxus Intra</h1>
|
<h1 id="login-title">Noxus Intra</h1>
|
||||||
@@ -79,6 +79,7 @@
|
|||||||
<button class="tab" data-tab="support">Asiakaspalvelu</button>
|
<button class="tab" data-tab="support">Asiakaspalvelu</button>
|
||||||
<button class="tab active" data-tab="customers">Asiakkaat</button>
|
<button class="tab active" data-tab="customers">Asiakkaat</button>
|
||||||
<button class="tab" data-tab="leads">Liidit</button>
|
<button class="tab" data-tab="leads">Liidit</button>
|
||||||
|
<button class="tab" data-tab="devices">Laitteet</button>
|
||||||
<button class="tab" data-tab="archive">Arkisto</button>
|
<button class="tab" data-tab="archive">Arkisto</button>
|
||||||
<button class="tab" data-tab="changelog">Muutosloki</button>
|
<button class="tab" data-tab="changelog">Muutosloki</button>
|
||||||
<button class="tab" data-tab="settings" id="tab-settings" style="display:none">API</button>
|
<button class="tab" data-tab="settings" id="tab-settings" style="display:none">API</button>
|
||||||
@@ -192,6 +193,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Laitteet -->
|
||||||
|
<div class="tab-content" id="tab-content-devices">
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="search-bar" style="display:flex;gap:0.5rem;align-items:center;">
|
||||||
|
<input type="text" id="device-search-input" placeholder="Hae laitteita..." style="flex:1;">
|
||||||
|
<button class="btn-primary" id="btn-add-device" style="white-space:nowrap;">+ Lisää laite</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-card">
|
||||||
|
<table id="device-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="nimi">Nimi ↕</th>
|
||||||
|
<th>Hallintaosoite</th>
|
||||||
|
<th>Serial</th>
|
||||||
|
<th>Sijainti</th>
|
||||||
|
<th>Funktio</th>
|
||||||
|
<th>Tyyppi</th>
|
||||||
|
<th>Malli</th>
|
||||||
|
<th>Ping</th>
|
||||||
|
<th>Toiminnot</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="device-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="no-devices" class="empty-state" style="display:none;">
|
||||||
|
<p>Ei laitteita vielä. Lisää ensimmäinen laite.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-bar">
|
||||||
|
<span id="device-count">0 laitetta</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tab: Arkisto -->
|
<!-- Tab: Arkisto -->
|
||||||
<div class="tab-content" id="tab-content-archive">
|
<div class="tab-content" id="tab-content-archive">
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
@@ -620,6 +655,9 @@
|
|||||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
||||||
<input type="checkbox" data-module="leads"> Liidit
|
<input type="checkbox" data-module="leads"> Liidit
|
||||||
</label>
|
</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
||||||
|
<input type="checkbox" data-module="devices"> Laitteet
|
||||||
|
</label>
|
||||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
||||||
<input type="checkbox" data-module="archive" checked> Arkisto
|
<input type="checkbox" data-module="archive" checked> Arkisto
|
||||||
</label>
|
</label>
|
||||||
@@ -689,6 +727,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Sijainnit (Sites) -->
|
||||||
|
<div class="table-card" style="padding:1.5rem;margin-top:1rem;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||||||
|
<h3 style="color:#0f3460;margin:0;">Sijainnit</h3>
|
||||||
|
<button class="btn-primary" id="btn-add-site" style="font-size:0.85rem;">+ Lisää sijainti</button>
|
||||||
|
</div>
|
||||||
|
<p style="color:#888;font-size:0.85rem;margin-bottom:1rem;">Toimipisteet ja konesalit joihin laitteita voidaan sijoittaa.</p>
|
||||||
|
<div id="sites-list"></div>
|
||||||
|
<div id="site-form-container" style="display:none;margin-top:1rem;padding:1rem;background:#f8f9fb;border-radius:8px;">
|
||||||
|
<h4 style="color:#0f3460;margin-bottom:0.75rem;" id="site-form-title">Uusi sijainti</h4>
|
||||||
|
<input type="hidden" id="site-form-id">
|
||||||
|
<div class="form-grid" style="max-width:600px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nimi *</label>
|
||||||
|
<input type="text" id="site-form-nimi" placeholder="esim. Konesali A">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Osoite</label>
|
||||||
|
<input type="text" id="site-form-osoite" placeholder="esim. Teollisuuskatu 5">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Kaupunki</label>
|
||||||
|
<input type="text" id="site-form-kaupunki" placeholder="esim. Helsinki">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
|
||||||
|
<button class="btn-primary" id="btn-save-site">Tallenna</button>
|
||||||
|
<button class="btn-secondary" id="btn-cancel-site">Peruuta</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Käyttäjäoikeudet -->
|
<!-- Käyttäjäoikeudet -->
|
||||||
<div class="table-card" style="padding:1.5rem;margin-top:1rem;">
|
<div class="table-card" style="padding:1.5rem;margin-top:1rem;">
|
||||||
<h3 style="color:#0f3460;margin-bottom:0.5rem;">Käyttäjäoikeudet</h3>
|
<h3 style="color:#0f3460;margin-bottom:0.5rem;">Käyttäjäoikeudet</h3>
|
||||||
@@ -705,6 +774,66 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Asiakas-modal -->
|
<!-- Asiakas-modal -->
|
||||||
|
<!-- Laite-modaali -->
|
||||||
|
<div id="device-modal" class="modal" style="display:none">
|
||||||
|
<div class="modal-content modal-wide">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="device-modal-title">Lisää laite</h2>
|
||||||
|
<button class="modal-close" id="device-modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="device-form">
|
||||||
|
<input type="hidden" id="device-form-id">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device-form-nimi">Nimi *</label>
|
||||||
|
<input type="text" id="device-form-nimi" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device-form-hallintaosoite">Hallintaosoite</label>
|
||||||
|
<input type="text" id="device-form-hallintaosoite" placeholder="10.0.0.1 tai https://...">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device-form-serial">Serial</label>
|
||||||
|
<input type="text" id="device-form-serial">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device-form-site">Sijainti</label>
|
||||||
|
<select id="device-form-site">
|
||||||
|
<option value="">— Ei sijaintia —</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device-form-funktio">Funktio</label>
|
||||||
|
<input type="text" id="device-form-funktio" placeholder="esim. Reititin, Kytkin, AP">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device-form-tyyppi">Tyyppi</label>
|
||||||
|
<input type="text" id="device-form-tyyppi" placeholder="esim. L3-kytkin">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device-form-malli">Malli</label>
|
||||||
|
<input type="text" id="device-form-malli" placeholder="esim. Cisco C9300">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ping-check</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:0.5rem;font-weight:normal;cursor:pointer;margin-top:0.25rem;">
|
||||||
|
<input type="checkbox" id="device-form-ping-check">
|
||||||
|
<span>Päällä</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="device-form-lisatiedot">Lisätiedot</label>
|
||||||
|
<textarea id="device-form-lisatiedot" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions" style="display:flex;gap:0.5rem;margin-top:1rem;">
|
||||||
|
<button type="submit" class="btn-primary">Tallenna</button>
|
||||||
|
<button type="button" class="btn-secondary" id="device-form-cancel">Peruuta</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="customer-modal" class="modal" style="display:none">
|
<div id="customer-modal" class="modal" style="display:none">
|
||||||
<div class="modal-content modal-wide">
|
<div class="modal-content modal-wide">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
|||||||
217
script.js
217
script.js
@@ -119,6 +119,7 @@ async function checkAuth() {
|
|||||||
// Tarkista onko URL:ssa reset-token
|
// Tarkista onko URL:ssa reset-token
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
if (params.get('reset')) {
|
if (params.get('reset')) {
|
||||||
|
loginScreen.style.display = 'flex';
|
||||||
try {
|
try {
|
||||||
const data = await apiCall('validate_reset_token&token=' + encodeURIComponent(params.get('reset')));
|
const data = await apiCall('validate_reset_token&token=' + encodeURIComponent(params.get('reset')));
|
||||||
if (data.valid) { showResetView(); return; }
|
if (data.valid) { showResetView(); return; }
|
||||||
@@ -139,8 +140,11 @@ async function checkAuth() {
|
|||||||
if (data.branding) applyBranding(data.branding);
|
if (data.branding) applyBranding(data.branding);
|
||||||
applyModules(data.enabled_modules || []);
|
applyModules(data.enabled_modules || []);
|
||||||
showDashboard();
|
showDashboard();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) { /* not logged in */ }
|
} catch (e) { /* not logged in */ }
|
||||||
|
// Ei kirjautuneena → näytä login
|
||||||
|
loginScreen.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
loginForm.addEventListener('submit', async (e) => {
|
loginForm.addEventListener('submit', async (e) => {
|
||||||
@@ -190,7 +194,7 @@ async function showDashboard() {
|
|||||||
populateCompanySelector();
|
populateCompanySelector();
|
||||||
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
|
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
|
||||||
const hash = window.location.hash.replace('#', '');
|
const hash = window.location.hash.replace('#', '');
|
||||||
const validTabs = ['customers', 'leads', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
|
const validTabs = ['customers', 'leads', 'devices', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
|
||||||
const startTab = validTabs.includes(hash) ? hash : 'customers';
|
const startTab = validTabs.includes(hash) ? hash : 'customers';
|
||||||
switchToTab(startTab);
|
switchToTab(startTab);
|
||||||
}
|
}
|
||||||
@@ -237,6 +241,7 @@ function switchToTab(target) {
|
|||||||
// Lataa sisältö tarvittaessa
|
// Lataa sisältö tarvittaessa
|
||||||
if (target === 'customers') loadCustomers();
|
if (target === 'customers') loadCustomers();
|
||||||
if (target === 'leads') loadLeads();
|
if (target === 'leads') loadLeads();
|
||||||
|
if (target === 'devices') loadDevices();
|
||||||
if (target === 'archive') loadArchive();
|
if (target === 'archive') loadArchive();
|
||||||
if (target === 'changelog') loadChangelog();
|
if (target === 'changelog') loadChangelog();
|
||||||
if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); }
|
if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); }
|
||||||
@@ -2075,6 +2080,8 @@ async function showCompanyDetail(id) {
|
|||||||
|
|
||||||
// Lataa postilaatikot
|
// Lataa postilaatikot
|
||||||
loadMailboxes();
|
loadMailboxes();
|
||||||
|
// Lataa sijainnit
|
||||||
|
loadSites();
|
||||||
// Lataa käyttäjäoikeudet
|
// Lataa käyttäjäoikeudet
|
||||||
loadCompanyUsers(id);
|
loadCompanyUsers(id);
|
||||||
}
|
}
|
||||||
@@ -2337,9 +2344,215 @@ async function toggleCompanyUser(userId, companyId, add) {
|
|||||||
} catch (e) { alert(e.message); }
|
} catch (e) { alert(e.message); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== LAITTEET (DEVICES) ====================
|
||||||
|
|
||||||
|
let devicesData = [];
|
||||||
|
let sitesData = [];
|
||||||
|
|
||||||
|
async function loadDevices() {
|
||||||
|
try {
|
||||||
|
devicesData = await apiCall('devices');
|
||||||
|
renderDevices();
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDevices() {
|
||||||
|
const query = (document.getElementById('device-search-input')?.value || '').toLowerCase().trim();
|
||||||
|
let filtered = devicesData;
|
||||||
|
if (query) {
|
||||||
|
filtered = devicesData.filter(d =>
|
||||||
|
(d.nimi || '').toLowerCase().includes(query) ||
|
||||||
|
(d.hallintaosoite || '').toLowerCase().includes(query) ||
|
||||||
|
(d.serial || '').toLowerCase().includes(query) ||
|
||||||
|
(d.site_name || '').toLowerCase().includes(query) ||
|
||||||
|
(d.funktio || '').toLowerCase().includes(query) ||
|
||||||
|
(d.tyyppi || '').toLowerCase().includes(query) ||
|
||||||
|
(d.malli || '').toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const tbody = document.getElementById('device-tbody');
|
||||||
|
const noDevices = document.getElementById('no-devices');
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
noDevices.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
noDevices.style.display = 'none';
|
||||||
|
tbody.innerHTML = filtered.map(d => {
|
||||||
|
const pingIcon = d.ping_check ? (d.ping_status === 'up' ? '🟢' : (d.ping_status === 'down' ? '🔴' : '⚪')) : '<span style="color:#ccc;">—</span>';
|
||||||
|
return `<tr>
|
||||||
|
<td><strong>${esc(d.nimi)}</strong></td>
|
||||||
|
<td><code style="font-size:0.82rem;">${esc(d.hallintaosoite || '-')}</code></td>
|
||||||
|
<td style="font-size:0.85rem;">${esc(d.serial || '-')}</td>
|
||||||
|
<td>${d.site_name ? esc(d.site_name) : '<span style="color:#ccc;">-</span>'}</td>
|
||||||
|
<td>${esc(d.funktio || '-')}</td>
|
||||||
|
<td>${esc(d.tyyppi || '-')}</td>
|
||||||
|
<td>${esc(d.malli || '-')}</td>
|
||||||
|
<td style="text-align:center;">${pingIcon}</td>
|
||||||
|
<td class="actions-cell">
|
||||||
|
<button class="btn-link" onclick="editDevice('${d.id}')">✎</button>
|
||||||
|
<button class="btn-link" style="color:#dc2626;" onclick="deleteDevice('${d.id}','${esc(d.nimi)}')">🗑</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
document.getElementById('device-count').textContent = filtered.length + ' laitetta' + (query ? ` (${devicesData.length} yhteensä)` : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editDevice(id) {
|
||||||
|
const d = devicesData.find(x => x.id === id);
|
||||||
|
if (!d) return;
|
||||||
|
document.getElementById('device-form-id').value = d.id;
|
||||||
|
document.getElementById('device-form-nimi').value = d.nimi || '';
|
||||||
|
document.getElementById('device-form-hallintaosoite').value = d.hallintaosoite || '';
|
||||||
|
document.getElementById('device-form-serial').value = d.serial || '';
|
||||||
|
document.getElementById('device-form-funktio').value = d.funktio || '';
|
||||||
|
document.getElementById('device-form-tyyppi').value = d.tyyppi || '';
|
||||||
|
document.getElementById('device-form-malli').value = d.malli || '';
|
||||||
|
document.getElementById('device-form-ping-check').checked = d.ping_check || false;
|
||||||
|
document.getElementById('device-form-lisatiedot').value = d.lisatiedot || '';
|
||||||
|
await loadSitesForDropdown();
|
||||||
|
document.getElementById('device-form-site').value = d.site_id || '';
|
||||||
|
document.getElementById('device-modal-title').textContent = 'Muokkaa laitetta';
|
||||||
|
document.getElementById('device-modal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDevice(id, name) {
|
||||||
|
if (!confirm(`Poistetaanko laite "${name}"?`)) return;
|
||||||
|
try {
|
||||||
|
await apiCall('device_delete', 'POST', { id });
|
||||||
|
loadDevices();
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSitesForDropdown() {
|
||||||
|
try {
|
||||||
|
sitesData = await apiCall('sites');
|
||||||
|
const sel = document.getElementById('device-form-site');
|
||||||
|
sel.innerHTML = '<option value="">— Ei sijaintia —</option>' +
|
||||||
|
sitesData.map(s => `<option value="${s.id}">${esc(s.nimi)}${s.kaupunki ? ' (' + esc(s.kaupunki) + ')' : ''}</option>`).join('');
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-add-device')?.addEventListener('click', async () => {
|
||||||
|
document.getElementById('device-form-id').value = '';
|
||||||
|
document.getElementById('device-form').reset();
|
||||||
|
await loadSitesForDropdown();
|
||||||
|
document.getElementById('device-modal-title').textContent = 'Lisää laite';
|
||||||
|
document.getElementById('device-modal').style.display = 'flex';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('device-modal-close')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('device-modal').style.display = 'none';
|
||||||
|
});
|
||||||
|
document.getElementById('device-form-cancel')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('device-modal').style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('device-form')?.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = document.getElementById('device-form-id').value;
|
||||||
|
const data = {
|
||||||
|
nimi: document.getElementById('device-form-nimi').value.trim(),
|
||||||
|
hallintaosoite: document.getElementById('device-form-hallintaosoite').value.trim(),
|
||||||
|
serial: document.getElementById('device-form-serial').value.trim(),
|
||||||
|
site_id: document.getElementById('device-form-site').value || null,
|
||||||
|
funktio: document.getElementById('device-form-funktio').value.trim(),
|
||||||
|
tyyppi: document.getElementById('device-form-tyyppi').value.trim(),
|
||||||
|
malli: document.getElementById('device-form-malli').value.trim(),
|
||||||
|
ping_check: document.getElementById('device-form-ping-check').checked,
|
||||||
|
lisatiedot: document.getElementById('device-form-lisatiedot').value.trim(),
|
||||||
|
};
|
||||||
|
if (id) data.id = id;
|
||||||
|
try {
|
||||||
|
await apiCall('device_save', 'POST', data);
|
||||||
|
document.getElementById('device-modal').style.display = 'none';
|
||||||
|
loadDevices();
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('device-search-input')?.addEventListener('input', () => renderDevices());
|
||||||
|
|
||||||
|
// ==================== SIJAINNIT (SITES) HALLINTA ====================
|
||||||
|
|
||||||
|
async function loadSites() {
|
||||||
|
try {
|
||||||
|
sitesData = await apiCall('sites');
|
||||||
|
renderSites();
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSites() {
|
||||||
|
const container = document.getElementById('sites-list');
|
||||||
|
if (!container) return;
|
||||||
|
if (sitesData.length === 0) {
|
||||||
|
container.innerHTML = '<p style="color:#888;font-size:0.9rem;">Ei sijainteja. Lisää ensimmäinen sijainti.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = sitesData.map(s => `<div style="display:flex;justify-content:space-between;align-items:center;padding:0.75rem;background:#fff;border:1px solid #e0e0e0;border-radius:8px;margin-bottom:0.5rem;">
|
||||||
|
<div>
|
||||||
|
<strong>${esc(s.nimi)}</strong>
|
||||||
|
${s.osoite ? `<span style="color:#888;font-size:0.85rem;margin-left:0.75rem;">${esc(s.osoite)}</span>` : ''}
|
||||||
|
${s.kaupunki ? `<span style="color:#888;font-size:0.85rem;margin-left:0.5rem;">${esc(s.kaupunki)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:0.5rem;">
|
||||||
|
<button class="btn-link" onclick="editSite('${s.id}')">Muokkaa</button>
|
||||||
|
<button class="btn-link" style="color:#dc2626;" onclick="deleteSite('${s.id}','${esc(s.nimi)}')">Poista</button>
|
||||||
|
</div>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function editSite(id) {
|
||||||
|
const s = sitesData.find(x => x.id === id);
|
||||||
|
if (!s) return;
|
||||||
|
document.getElementById('site-form-id').value = s.id;
|
||||||
|
document.getElementById('site-form-nimi').value = s.nimi || '';
|
||||||
|
document.getElementById('site-form-osoite').value = s.osoite || '';
|
||||||
|
document.getElementById('site-form-kaupunki').value = s.kaupunki || '';
|
||||||
|
document.getElementById('site-form-title').textContent = 'Muokkaa sijaintia';
|
||||||
|
document.getElementById('site-form-container').style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSite(id, name) {
|
||||||
|
if (!confirm(`Poistetaanko sijainti "${name}"? Laitteet joissa tämä sijainti on menettävät sijainti-viittauksen.`)) return;
|
||||||
|
try {
|
||||||
|
await apiCall('site_delete', 'POST', { id });
|
||||||
|
loadSites();
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-add-site')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('site-form-id').value = '';
|
||||||
|
document.getElementById('site-form-nimi').value = '';
|
||||||
|
document.getElementById('site-form-osoite').value = '';
|
||||||
|
document.getElementById('site-form-kaupunki').value = '';
|
||||||
|
document.getElementById('site-form-title').textContent = 'Uusi sijainti';
|
||||||
|
document.getElementById('site-form-container').style.display = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-save-site')?.addEventListener('click', async () => {
|
||||||
|
const id = document.getElementById('site-form-id').value;
|
||||||
|
const nimi = document.getElementById('site-form-nimi').value.trim();
|
||||||
|
if (!nimi) return alert('Sijainnin nimi vaaditaan');
|
||||||
|
const data = {
|
||||||
|
nimi,
|
||||||
|
osoite: document.getElementById('site-form-osoite').value.trim(),
|
||||||
|
kaupunki: document.getElementById('site-form-kaupunki').value.trim(),
|
||||||
|
};
|
||||||
|
if (id) data.id = id;
|
||||||
|
try {
|
||||||
|
await apiCall('site_save', 'POST', data);
|
||||||
|
document.getElementById('site-form-container').style.display = 'none';
|
||||||
|
loadSites();
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-cancel-site')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('site-form-container').style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== MODUULIT ====================
|
// ==================== MODUULIT ====================
|
||||||
|
|
||||||
const ALL_MODULES = ['customers', 'support', 'leads', 'archive', 'changelog', 'settings'];
|
const ALL_MODULES = ['customers', 'support', 'leads', 'devices', 'archive', 'changelog', 'settings'];
|
||||||
const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings'];
|
const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings'];
|
||||||
|
|
||||||
function applyModules(modules) {
|
function applyModules(modules) {
|
||||||
|
|||||||
Reference in New Issue
Block a user