Add address fields, e-invoice, stats and auto-backup
- Split address into street, postal code, city (sortable) - Add billing postal code/city fields - Add e-invoice address and operator fields - Add trivia stats (top postal code, top speed, avg price) - Improved layout with stat cards grid and max-width container - Sticky header, modal animations, search icon - Auto-backup on every save (keeps last 30 backups) - Footer added Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
data/customers.json
|
data/customers.json
|
||||||
|
data/backups/
|
||||||
|
|||||||
24
api.php
24
api.php
@@ -31,6 +31,18 @@ function loadCustomers(): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveCustomers(array $customers): void {
|
function saveCustomers(array $customers): void {
|
||||||
|
// Automaattinen backup ennen tallennusta
|
||||||
|
if (file_exists(DATA_FILE) && filesize(DATA_FILE) > 2) {
|
||||||
|
$backupDir = __DIR__ . '/data/backups';
|
||||||
|
if (!file_exists($backupDir)) mkdir($backupDir, 0755, true);
|
||||||
|
copy(DATA_FILE, $backupDir . '/customers_' . date('Y-m-d_His') . '.json');
|
||||||
|
// Säilytä vain 30 viimeisintä backuppia
|
||||||
|
$backups = glob($backupDir . '/customers_*.json');
|
||||||
|
if (count($backups) > 30) {
|
||||||
|
sort($backups);
|
||||||
|
array_map('unlink', array_slice($backups, 0, count($backups) - 30));
|
||||||
|
}
|
||||||
|
}
|
||||||
file_put_contents(DATA_FILE, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
file_put_contents(DATA_FILE, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,13 +90,19 @@ switch ($action) {
|
|||||||
'id' => generateId(),
|
'id' => generateId(),
|
||||||
'yritys' => trim($input['yritys'] ?? ''),
|
'yritys' => trim($input['yritys'] ?? ''),
|
||||||
'asennusosoite' => trim($input['asennusosoite'] ?? ''),
|
'asennusosoite' => trim($input['asennusosoite'] ?? ''),
|
||||||
|
'postinumero' => trim($input['postinumero'] ?? ''),
|
||||||
|
'kaupunki' => trim($input['kaupunki'] ?? ''),
|
||||||
'liittymanopeus' => trim($input['liittymanopeus'] ?? ''),
|
'liittymanopeus' => trim($input['liittymanopeus'] ?? ''),
|
||||||
'hinta' => floatval($input['hinta'] ?? 0),
|
'hinta' => floatval($input['hinta'] ?? 0),
|
||||||
'yhteyshenkilö' => trim($input['yhteyshenkilö'] ?? ''),
|
'yhteyshenkilö' => trim($input['yhteyshenkilö'] ?? ''),
|
||||||
'puhelin' => trim($input['puhelin'] ?? ''),
|
'puhelin' => trim($input['puhelin'] ?? ''),
|
||||||
'sahkoposti' => trim($input['sahkoposti'] ?? ''),
|
'sahkoposti' => trim($input['sahkoposti'] ?? ''),
|
||||||
'laskutusosoite' => trim($input['laskutusosoite'] ?? ''),
|
'laskutusosoite' => trim($input['laskutusosoite'] ?? ''),
|
||||||
|
'laskutuspostinumero' => trim($input['laskutuspostinumero'] ?? ''),
|
||||||
|
'laskutuskaupunki' => trim($input['laskutuskaupunki'] ?? ''),
|
||||||
'laskutussahkoposti' => trim($input['laskutussahkoposti'] ?? ''),
|
'laskutussahkoposti' => trim($input['laskutussahkoposti'] ?? ''),
|
||||||
|
'elaskuosoite' => trim($input['elaskuosoite'] ?? ''),
|
||||||
|
'elaskuvalittaja' => trim($input['elaskuvalittaja'] ?? ''),
|
||||||
'ytunnus' => trim($input['ytunnus'] ?? ''),
|
'ytunnus' => trim($input['ytunnus'] ?? ''),
|
||||||
'lisatiedot' => trim($input['lisatiedot'] ?? ''),
|
'lisatiedot' => trim($input['lisatiedot'] ?? ''),
|
||||||
'luotu' => date('Y-m-d H:i:s'),
|
'luotu' => date('Y-m-d H:i:s'),
|
||||||
@@ -111,13 +129,19 @@ switch ($action) {
|
|||||||
if ($c['id'] === $id) {
|
if ($c['id'] === $id) {
|
||||||
$c['yritys'] = trim($input['yritys'] ?? $c['yritys']);
|
$c['yritys'] = trim($input['yritys'] ?? $c['yritys']);
|
||||||
$c['asennusosoite'] = trim($input['asennusosoite'] ?? $c['asennusosoite']);
|
$c['asennusosoite'] = trim($input['asennusosoite'] ?? $c['asennusosoite']);
|
||||||
|
$c['postinumero'] = trim($input['postinumero'] ?? ($c['postinumero'] ?? ''));
|
||||||
|
$c['kaupunki'] = trim($input['kaupunki'] ?? ($c['kaupunki'] ?? ''));
|
||||||
$c['liittymanopeus'] = trim($input['liittymanopeus'] ?? $c['liittymanopeus']);
|
$c['liittymanopeus'] = trim($input['liittymanopeus'] ?? $c['liittymanopeus']);
|
||||||
$c['hinta'] = floatval($input['hinta'] ?? $c['hinta']);
|
$c['hinta'] = floatval($input['hinta'] ?? $c['hinta']);
|
||||||
$c['yhteyshenkilö'] = trim($input['yhteyshenkilö'] ?? $c['yhteyshenkilö']);
|
$c['yhteyshenkilö'] = trim($input['yhteyshenkilö'] ?? $c['yhteyshenkilö']);
|
||||||
$c['puhelin'] = trim($input['puhelin'] ?? $c['puhelin']);
|
$c['puhelin'] = trim($input['puhelin'] ?? $c['puhelin']);
|
||||||
$c['sahkoposti'] = trim($input['sahkoposti'] ?? $c['sahkoposti']);
|
$c['sahkoposti'] = trim($input['sahkoposti'] ?? $c['sahkoposti']);
|
||||||
$c['laskutusosoite'] = trim($input['laskutusosoite'] ?? $c['laskutusosoite']);
|
$c['laskutusosoite'] = trim($input['laskutusosoite'] ?? $c['laskutusosoite']);
|
||||||
|
$c['laskutuspostinumero'] = trim($input['laskutuspostinumero'] ?? ($c['laskutuspostinumero'] ?? ''));
|
||||||
|
$c['laskutuskaupunki'] = trim($input['laskutuskaupunki'] ?? ($c['laskutuskaupunki'] ?? ''));
|
||||||
$c['laskutussahkoposti'] = trim($input['laskutussahkoposti'] ?? $c['laskutussahkoposti']);
|
$c['laskutussahkoposti'] = trim($input['laskutussahkoposti'] ?? $c['laskutussahkoposti']);
|
||||||
|
$c['elaskuosoite'] = trim($input['elaskuosoite'] ?? ($c['elaskuosoite'] ?? ''));
|
||||||
|
$c['elaskuvalittaja'] = trim($input['elaskuvalittaja'] ?? ($c['elaskuvalittaja'] ?? ''));
|
||||||
$c['ytunnus'] = trim($input['ytunnus'] ?? $c['ytunnus']);
|
$c['ytunnus'] = trim($input['ytunnus'] ?? $c['ytunnus']);
|
||||||
$c['lisatiedot'] = trim($input['lisatiedot'] ?? $c['lisatiedot']);
|
$c['lisatiedot'] = trim($input['lisatiedot'] ?? $c['lisatiedot']);
|
||||||
$c['muokattu'] = date('Y-m-d H:i:s');
|
$c['muokattu'] = date('Y-m-d H:i:s');
|
||||||
|
|||||||
140
index.html
140
index.html
@@ -24,8 +24,13 @@
|
|||||||
<div id="dashboard" style="display:none">
|
<div id="dashboard" style="display:none">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1>CuituNet Intra</h1>
|
<div class="header-brand">
|
||||||
<span class="subtitle">Asiakashallinta</span>
|
<span class="brand-icon">⚡</span>
|
||||||
|
<div>
|
||||||
|
<h1>CuituNet Intra</h1>
|
||||||
|
<span class="subtitle">Kuituasiakkaiden hallinta</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button id="btn-add" class="btn-primary">+ Lisää asiakas</button>
|
<button id="btn-add" class="btn-primary">+ Lisää asiakas</button>
|
||||||
@@ -33,35 +38,78 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Haku -->
|
<div class="main-container">
|
||||||
<div class="search-bar">
|
<!-- Stat-kortit -->
|
||||||
<input type="text" id="search-input" placeholder="Hae yrityksen nimellä tai osoitteella...">
|
<div class="stats-row">
|
||||||
</div>
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Asiakkaita</div>
|
||||||
|
<div class="stat-value" id="stat-count">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Laskutus / kk</div>
|
||||||
|
<div class="stat-value stat-highlight" id="stat-billing">0,00 €</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Laskutus / vuosi</div>
|
||||||
|
<div class="stat-value" id="stat-yearly">0,00 €</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-trivia">
|
||||||
|
<div class="stat-label">Suosituin postinumero</div>
|
||||||
|
<div class="stat-value" id="stat-top-zip">-</div>
|
||||||
|
<div class="stat-sub" id="stat-top-zip-detail"></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-trivia">
|
||||||
|
<div class="stat-label">Suosituin nopeus</div>
|
||||||
|
<div class="stat-value" id="stat-top-speed">-</div>
|
||||||
|
<div class="stat-sub" id="stat-top-speed-detail"></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-trivia">
|
||||||
|
<div class="stat-label">Keskihinta / kk</div>
|
||||||
|
<div class="stat-value" id="stat-avg-price">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Taulukko -->
|
<!-- Toolbar: haku + info -->
|
||||||
<div class="table-container">
|
<div class="toolbar">
|
||||||
<table id="customer-table">
|
<div class="search-bar">
|
||||||
<thead>
|
<span class="search-icon">🔍</span>
|
||||||
<tr>
|
<input type="text" id="search-input" placeholder="Hae yrityksen nimellä, osoitteella tai yhteyshenkilöllä...">
|
||||||
<th data-sort="yritys">Yritys ↕</th>
|
</div>
|
||||||
<th data-sort="asennusosoite">Asennusosoite ↕</th>
|
</div>
|
||||||
<th data-sort="liittymanopeus">Nopeus ↕</th>
|
|
||||||
<th data-sort="hinta">Hinta/kk ↕</th>
|
<!-- Taulukko -->
|
||||||
<th>Toiminnot</th>
|
<div class="table-card">
|
||||||
</tr>
|
<table id="customer-table">
|
||||||
</thead>
|
<thead>
|
||||||
<tbody id="customer-tbody"></tbody>
|
<tr>
|
||||||
</table>
|
<th data-sort="yritys">Yritys ↕</th>
|
||||||
<div id="no-customers" class="empty-state" style="display:none">
|
<th data-sort="asennusosoite">Osoite ↕</th>
|
||||||
<p>Ei asiakkaita vielä. Lisää ensimmäinen asiakas!</p>
|
<th data-sort="postinumero">Postinro ↕</th>
|
||||||
|
<th data-sort="kaupunki">Kaupunki ↕</th>
|
||||||
|
<th data-sort="liittymanopeus">Nopeus ↕</th>
|
||||||
|
<th data-sort="hinta">Hinta/kk ↕</th>
|
||||||
|
<th>Toiminnot</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="customer-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="no-customers" class="empty-state" style="display:none">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<p>Ei asiakkaita vielä.</p>
|
||||||
|
<p class="empty-hint">Klikkaa "Lisää asiakas" lisätäksesi ensimmäisen asiakkaan.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Yhteenveto -->
|
||||||
|
<div class="summary-bar">
|
||||||
|
<span id="customer-count">0 asiakasta</span>
|
||||||
|
<span id="total-billing">Laskutus yhteensä: 0,00 €/kk</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Yhteenveto -->
|
<footer>
|
||||||
<div class="summary-bar">
|
<p>CuituNet Intra — Asiakashallintajärjestelmä</p>
|
||||||
<span id="customer-count">0 asiakasta</span>
|
</footer>
|
||||||
<span id="total-billing">Laskutus yhteensä: 0,00 €/kk</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Asiakas-modal -->
|
<!-- Asiakas-modal -->
|
||||||
@@ -84,10 +132,26 @@
|
|||||||
<label for="form-ytunnus">Y-tunnus</label>
|
<label for="form-ytunnus">Y-tunnus</label>
|
||||||
<input type="text" id="form-ytunnus" placeholder="1234567-8">
|
<input type="text" id="form-ytunnus" placeholder="1234567-8">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="form-asennusosoite">Asennusosoite</label>
|
|
||||||
<input type="text" id="form-asennusosoite">
|
<h3>Asennusosoite</h3>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="form-asennusosoite">Osoite</label>
|
||||||
|
<input type="text" id="form-asennusosoite" placeholder="esim. Kauppakatu 5">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="form-postinumero">Postinumero</label>
|
||||||
|
<input type="text" id="form-postinumero" placeholder="20100">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="form-kaupunki">Kaupunki</label>
|
||||||
|
<input type="text" id="form-kaupunki" placeholder="Turku">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Liittymä</h3>
|
||||||
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="form-liittymanopeus">Liittymänopeus</label>
|
<label for="form-liittymanopeus">Liittymänopeus</label>
|
||||||
<input type="text" id="form-liittymanopeus" placeholder="esim. 100/100">
|
<input type="text" id="form-liittymanopeus" placeholder="esim. 100/100">
|
||||||
@@ -120,10 +184,26 @@
|
|||||||
<label for="form-laskutusosoite">Laskutusosoite</label>
|
<label for="form-laskutusosoite">Laskutusosoite</label>
|
||||||
<input type="text" id="form-laskutusosoite">
|
<input type="text" id="form-laskutusosoite">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="form-laskutuspostinumero">Postinumero</label>
|
||||||
|
<input type="text" id="form-laskutuspostinumero" placeholder="20100">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="form-laskutuskaupunki">Kaupunki</label>
|
||||||
|
<input type="text" id="form-laskutuskaupunki">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="form-laskutussahkoposti">Laskutussähköposti</label>
|
<label for="form-laskutussahkoposti">Laskutussähköposti</label>
|
||||||
<input type="email" id="form-laskutussahkoposti">
|
<input type="email" id="form-laskutussahkoposti">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="form-elaskuosoite">E-laskuosoite</label>
|
||||||
|
<input type="text" id="form-elaskuosoite" placeholder="esim. 003712345678">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="form-elaskuvalittaja">E-laskuvälittäjä</label>
|
||||||
|
<input type="text" id="form-elaskuvalittaja" placeholder="esim. DABAFIHH">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Lisätiedot</h3>
|
<h3>Lisätiedot</h3>
|
||||||
|
|||||||
160
script.js
160
script.js
@@ -79,8 +79,11 @@ function renderTable() {
|
|||||||
if (query) {
|
if (query) {
|
||||||
filtered = customers.filter(c =>
|
filtered = customers.filter(c =>
|
||||||
c.yritys.toLowerCase().includes(query) ||
|
c.yritys.toLowerCase().includes(query) ||
|
||||||
c.asennusosoite.toLowerCase().includes(query) ||
|
(c.asennusosoite || '').toLowerCase().includes(query) ||
|
||||||
(c.yhteyshenkilö && c.yhteyshenkilö.toLowerCase().includes(query))
|
(c.postinumero || '').toLowerCase().includes(query) ||
|
||||||
|
(c.kaupunki || '').toLowerCase().includes(query) ||
|
||||||
|
(c.yhteyshenkilö || '').toLowerCase().includes(query) ||
|
||||||
|
(c.liittymanopeus || '').toLowerCase().includes(query)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +112,10 @@ function renderTable() {
|
|||||||
document.getElementById('customer-table').style.display = 'table';
|
document.getElementById('customer-table').style.display = 'table';
|
||||||
tbody.innerHTML = filtered.map(c => `
|
tbody.innerHTML = filtered.map(c => `
|
||||||
<tr data-id="${c.id}">
|
<tr data-id="${c.id}">
|
||||||
<td>${esc(c.yritys)}</td>
|
<td><strong>${esc(c.yritys)}</strong></td>
|
||||||
<td>${esc(c.asennusosoite)}</td>
|
<td>${esc(c.asennusosoite)}</td>
|
||||||
|
<td>${esc(c.postinumero)}</td>
|
||||||
|
<td>${esc(c.kaupunki)}</td>
|
||||||
<td>${esc(c.liittymanopeus)}</td>
|
<td>${esc(c.liittymanopeus)}</td>
|
||||||
<td class="price-cell">${formatPrice(c.hinta)}</td>
|
<td class="price-cell">${formatPrice(c.hinta)}</td>
|
||||||
<td class="actions-cell">
|
<td class="actions-cell">
|
||||||
@@ -129,6 +134,70 @@ function updateSummary(filtered) {
|
|||||||
const total = customers.reduce((sum, c) => sum + (parseFloat(c.hinta) || 0), 0);
|
const total = customers.reduce((sum, c) => sum + (parseFloat(c.hinta) || 0), 0);
|
||||||
customerCount.textContent = `${count} asiakasta`;
|
customerCount.textContent = `${count} asiakasta`;
|
||||||
totalBilling.textContent = `Laskutus yhteensä: ${formatPrice(total)}/kk`;
|
totalBilling.textContent = `Laskutus yhteensä: ${formatPrice(total)}/kk`;
|
||||||
|
|
||||||
|
// Stat-kortit
|
||||||
|
const statCount = document.getElementById('stat-count');
|
||||||
|
const statBilling = document.getElementById('stat-billing');
|
||||||
|
const statYearly = document.getElementById('stat-yearly');
|
||||||
|
if (statCount) statCount.textContent = count;
|
||||||
|
if (statBilling) statBilling.textContent = formatPrice(total);
|
||||||
|
if (statYearly) statYearly.textContent = formatPrice(total * 12);
|
||||||
|
|
||||||
|
// Nippelitilastot
|
||||||
|
updateTrivia();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTrivia() {
|
||||||
|
const count = customers.length;
|
||||||
|
if (count === 0) {
|
||||||
|
setTrivia('stat-top-zip', '-', '');
|
||||||
|
setTrivia('stat-top-speed', '-', '');
|
||||||
|
setText('stat-avg-price', '-');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suosituin postinumero
|
||||||
|
const zipCounts = {};
|
||||||
|
customers.forEach(c => {
|
||||||
|
const zip = (c.postinumero || '').trim();
|
||||||
|
if (zip) zipCounts[zip] = (zipCounts[zip] || 0) + 1;
|
||||||
|
});
|
||||||
|
const topZip = Object.entries(zipCounts).sort((a, b) => b[1] - a[1])[0];
|
||||||
|
if (topZip) {
|
||||||
|
const city = customers.find(c => (c.postinumero || '').trim() === topZip[0]);
|
||||||
|
setTrivia('stat-top-zip', topZip[0], `${topZip[1]} liittymää` + (city && city.kaupunki ? ` (${city.kaupunki})` : ''));
|
||||||
|
} else {
|
||||||
|
setTrivia('stat-top-zip', '-', 'ei postinumeroita');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suosituin nopeus
|
||||||
|
const speedCounts = {};
|
||||||
|
customers.forEach(c => {
|
||||||
|
const speed = (c.liittymanopeus || '').trim();
|
||||||
|
if (speed) speedCounts[speed] = (speedCounts[speed] || 0) + 1;
|
||||||
|
});
|
||||||
|
const topSpeed = Object.entries(speedCounts).sort((a, b) => b[1] - a[1])[0];
|
||||||
|
if (topSpeed) {
|
||||||
|
setTrivia('stat-top-speed', topSpeed[0], `${topSpeed[1]} liittymää`);
|
||||||
|
} else {
|
||||||
|
setTrivia('stat-top-speed', '-', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keskihinta
|
||||||
|
const total = customers.reduce((sum, c) => sum + (parseFloat(c.hinta) || 0), 0);
|
||||||
|
setText('stat-avg-price', formatPrice(total / count));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTrivia(id, value, sub) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
const subEl = document.getElementById(id + '-detail');
|
||||||
|
if (el) el.textContent = value;
|
||||||
|
if (subEl) subEl.textContent = sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setText(id, value) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPrice(val) {
|
function formatPrice(val) {
|
||||||
@@ -167,34 +236,48 @@ tbody.addEventListener('click', (e) => {
|
|||||||
showDetail(id);
|
showDetail(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function detailVal(val) {
|
||||||
|
return val ? esc(val) : '<span class="empty">-</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailLink(val, type) {
|
||||||
|
if (!val) return '<span class="empty">-</span>';
|
||||||
|
if (type === 'tel') return `<a href="tel:${esc(val)}">${esc(val)}</a>`;
|
||||||
|
if (type === 'email') return `<a href="mailto:${esc(val)}">${esc(val)}</a>`;
|
||||||
|
return esc(val);
|
||||||
|
}
|
||||||
|
|
||||||
function showDetail(id) {
|
function showDetail(id) {
|
||||||
const c = customers.find(x => x.id === id);
|
const c = customers.find(x => x.id === id);
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
currentDetailId = id;
|
currentDetailId = id;
|
||||||
|
|
||||||
|
const fullAddress = [c.asennusosoite, c.postinumero, c.kaupunki].filter(Boolean).join(', ');
|
||||||
|
const fullBillingAddress = [c.laskutusosoite, c.laskutuspostinumero, c.laskutuskaupunki].filter(Boolean).join(', ');
|
||||||
|
|
||||||
document.getElementById('detail-title').textContent = c.yritys;
|
document.getElementById('detail-title').textContent = c.yritys;
|
||||||
document.getElementById('detail-body').innerHTML = `
|
document.getElementById('detail-body').innerHTML = `
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h3>Liittymätiedot</h3>
|
<h3>Yritys ja liittymä</h3>
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Yritys</div>
|
<div class="detail-label">Yritys</div>
|
||||||
<div class="detail-value">${esc(c.yritys) || '<span class="empty">-</span>'}</div>
|
<div class="detail-value">${detailVal(c.yritys)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Y-tunnus</div>
|
<div class="detail-label">Y-tunnus</div>
|
||||||
<div class="detail-value">${esc(c.ytunnus) || '<span class="empty">-</span>'}</div>
|
<div class="detail-value">${detailVal(c.ytunnus)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Asennusosoite</div>
|
<div class="detail-label">Asennusosoite</div>
|
||||||
<div class="detail-value">${esc(c.asennusosoite) || '<span class="empty">-</span>'}</div>
|
<div class="detail-value">${detailVal(fullAddress)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Nopeus</div>
|
<div class="detail-label">Nopeus</div>
|
||||||
<div class="detail-value">${esc(c.liittymanopeus) || '<span class="empty">-</span>'}</div>
|
<div class="detail-value">${detailVal(c.liittymanopeus)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Hinta</div>
|
<div class="detail-label">Hinta / kk</div>
|
||||||
<div class="detail-value price-cell">${formatPrice(c.hinta)}</div>
|
<div class="detail-value price-cell">${formatPrice(c.hinta)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,15 +287,15 @@ function showDetail(id) {
|
|||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Yhteyshenkilö</div>
|
<div class="detail-label">Yhteyshenkilö</div>
|
||||||
<div class="detail-value">${esc(c.yhteyshenkilö) || '<span class="empty">-</span>'}</div>
|
<div class="detail-value">${detailVal(c.yhteyshenkilö)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Puhelin</div>
|
<div class="detail-label">Puhelin</div>
|
||||||
<div class="detail-value">${c.puhelin ? `<a href="tel:${esc(c.puhelin)}">${esc(c.puhelin)}</a>` : '<span class="empty">-</span>'}</div>
|
<div class="detail-value">${detailLink(c.puhelin, 'tel')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Sähköposti</div>
|
<div class="detail-label">Sähköposti</div>
|
||||||
<div class="detail-value">${c.sahkoposti ? `<a href="mailto:${esc(c.sahkoposti)}">${esc(c.sahkoposti)}</a>` : '<span class="empty">-</span>'}</div>
|
<div class="detail-value">${detailLink(c.sahkoposti, 'email')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,11 +304,19 @@ function showDetail(id) {
|
|||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Laskutusosoite</div>
|
<div class="detail-label">Laskutusosoite</div>
|
||||||
<div class="detail-value">${esc(c.laskutusosoite) || '<span class="empty">-</span>'}</div>
|
<div class="detail-value">${detailVal(fullBillingAddress)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<div class="detail-label">Laskutussähköposti</div>
|
<div class="detail-label">Laskutussähköposti</div>
|
||||||
<div class="detail-value">${c.laskutussahkoposti ? `<a href="mailto:${esc(c.laskutussahkoposti)}">${esc(c.laskutussahkoposti)}</a>` : '<span class="empty">-</span>'}</div>
|
<div class="detail-value">${detailLink(c.laskutussahkoposti, 'email')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="detail-label">E-laskuosoite</div>
|
||||||
|
<div class="detail-value">${detailVal(c.elaskuosoite)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="detail-label">E-laskuvälittäjä</div>
|
||||||
|
<div class="detail-value">${detailVal(c.elaskuvalittaja)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,20 +351,27 @@ document.getElementById('modal-close').addEventListener('click', () => customerM
|
|||||||
document.getElementById('form-cancel').addEventListener('click', () => customerModal.style.display = 'none');
|
document.getElementById('form-cancel').addEventListener('click', () => customerModal.style.display = 'none');
|
||||||
|
|
||||||
function openCustomerForm(customer = null) {
|
function openCustomerForm(customer = null) {
|
||||||
document.getElementById('modal-title').textContent = customer ? 'Muokkaa asiakasta' : 'Lisää asiakas';
|
const c = customer;
|
||||||
document.getElementById('form-submit').textContent = customer ? 'Päivitä' : 'Tallenna';
|
document.getElementById('modal-title').textContent = c ? 'Muokkaa asiakasta' : 'Lisää asiakas';
|
||||||
document.getElementById('form-id').value = customer ? customer.id : '';
|
document.getElementById('form-submit').textContent = c ? 'Päivitä' : 'Tallenna';
|
||||||
document.getElementById('form-yritys').value = customer ? customer.yritys : '';
|
document.getElementById('form-id').value = c ? c.id : '';
|
||||||
document.getElementById('form-ytunnus').value = customer ? customer.ytunnus : '';
|
document.getElementById('form-yritys').value = c ? c.yritys : '';
|
||||||
document.getElementById('form-asennusosoite').value = customer ? customer.asennusosoite : '';
|
document.getElementById('form-ytunnus').value = c ? c.ytunnus : '';
|
||||||
document.getElementById('form-liittymanopeus').value = customer ? customer.liittymanopeus : '';
|
document.getElementById('form-asennusosoite').value = c ? c.asennusosoite : '';
|
||||||
document.getElementById('form-hinta').value = customer ? customer.hinta : '';
|
document.getElementById('form-postinumero').value = c ? (c.postinumero || '') : '';
|
||||||
document.getElementById('form-yhteyshenkilo').value = customer ? customer.yhteyshenkilö : '';
|
document.getElementById('form-kaupunki').value = c ? (c.kaupunki || '') : '';
|
||||||
document.getElementById('form-puhelin').value = customer ? customer.puhelin : '';
|
document.getElementById('form-liittymanopeus').value = c ? c.liittymanopeus : '';
|
||||||
document.getElementById('form-sahkoposti').value = customer ? customer.sahkoposti : '';
|
document.getElementById('form-hinta').value = c ? c.hinta : '';
|
||||||
document.getElementById('form-laskutusosoite').value = customer ? customer.laskutusosoite : '';
|
document.getElementById('form-yhteyshenkilo').value = c ? c.yhteyshenkilö : '';
|
||||||
document.getElementById('form-laskutussahkoposti').value = customer ? customer.laskutussahkoposti : '';
|
document.getElementById('form-puhelin').value = c ? c.puhelin : '';
|
||||||
document.getElementById('form-lisatiedot').value = customer ? customer.lisatiedot : '';
|
document.getElementById('form-sahkoposti').value = c ? c.sahkoposti : '';
|
||||||
|
document.getElementById('form-laskutusosoite').value = c ? c.laskutusosoite : '';
|
||||||
|
document.getElementById('form-laskutuspostinumero').value = c ? (c.laskutuspostinumero || '') : '';
|
||||||
|
document.getElementById('form-laskutuskaupunki').value = c ? (c.laskutuskaupunki || '') : '';
|
||||||
|
document.getElementById('form-laskutussahkoposti').value = c ? c.laskutussahkoposti : '';
|
||||||
|
document.getElementById('form-elaskuosoite').value = c ? (c.elaskuosoite || '') : '';
|
||||||
|
document.getElementById('form-elaskuvalittaja').value = c ? (c.elaskuvalittaja || '') : '';
|
||||||
|
document.getElementById('form-lisatiedot').value = c ? c.lisatiedot : '';
|
||||||
customerModal.style.display = 'flex';
|
customerModal.style.display = 'flex';
|
||||||
document.getElementById('form-yritys').focus();
|
document.getElementById('form-yritys').focus();
|
||||||
}
|
}
|
||||||
@@ -296,13 +394,19 @@ customerForm.addEventListener('submit', async (e) => {
|
|||||||
yritys: document.getElementById('form-yritys').value,
|
yritys: document.getElementById('form-yritys').value,
|
||||||
ytunnus: document.getElementById('form-ytunnus').value,
|
ytunnus: document.getElementById('form-ytunnus').value,
|
||||||
asennusosoite: document.getElementById('form-asennusosoite').value,
|
asennusosoite: document.getElementById('form-asennusosoite').value,
|
||||||
|
postinumero: document.getElementById('form-postinumero').value,
|
||||||
|
kaupunki: document.getElementById('form-kaupunki').value,
|
||||||
liittymanopeus: document.getElementById('form-liittymanopeus').value,
|
liittymanopeus: document.getElementById('form-liittymanopeus').value,
|
||||||
hinta: document.getElementById('form-hinta').value,
|
hinta: document.getElementById('form-hinta').value,
|
||||||
yhteyshenkilö: document.getElementById('form-yhteyshenkilo').value,
|
yhteyshenkilö: document.getElementById('form-yhteyshenkilo').value,
|
||||||
puhelin: document.getElementById('form-puhelin').value,
|
puhelin: document.getElementById('form-puhelin').value,
|
||||||
sahkoposti: document.getElementById('form-sahkoposti').value,
|
sahkoposti: document.getElementById('form-sahkoposti').value,
|
||||||
laskutusosoite: document.getElementById('form-laskutusosoite').value,
|
laskutusosoite: document.getElementById('form-laskutusosoite').value,
|
||||||
|
laskutuspostinumero: document.getElementById('form-laskutuspostinumero').value,
|
||||||
|
laskutuskaupunki: document.getElementById('form-laskutuskaupunki').value,
|
||||||
laskutussahkoposti: document.getElementById('form-laskutussahkoposti').value,
|
laskutussahkoposti: document.getElementById('form-laskutussahkoposti').value,
|
||||||
|
elaskuosoite: document.getElementById('form-elaskuosoite').value,
|
||||||
|
elaskuvalittaja: document.getElementById('form-elaskuvalittaja').value,
|
||||||
lisatiedot: document.getElementById('form-lisatiedot').value,
|
lisatiedot: document.getElementById('form-lisatiedot').value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
472
style.css
472
style.css
@@ -28,6 +28,7 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box h1 {
|
.login-box h1 {
|
||||||
@@ -80,27 +81,260 @@ body {
|
|||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
header {
|
header {
|
||||||
background: #0f3460;
|
background: linear-gradient(135deg, #0f3460, #16213e);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left h1 {
|
.header-left h1 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.3rem;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 0.85rem;
|
font-size: 0.8rem;
|
||||||
opacity: 0.8;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-right {
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main container */
|
||||||
|
.main-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat cards */
|
||||||
|
.stats-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-highlight {
|
||||||
|
color: #0f3460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-trivia {
|
||||||
|
background: #f8f9fb;
|
||||||
|
border: 1px dashed #d5dbe5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-trivia .stat-value {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-sub {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar */
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px 12px 40px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
background: #fff;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0f3460;
|
||||||
|
box-shadow: 0 0 0 3px rgba(15,52,96,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table card */
|
||||||
|
.table-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
background: #16213e;
|
||||||
|
color: #fff;
|
||||||
|
padding: 13px 16px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th:hover {
|
||||||
|
background: #1a2744;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background: #f4f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
padding: 13px 16px;
|
||||||
|
font-size: 0.93rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-cell {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f3460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-cell button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-cell button:hover {
|
||||||
|
background: #eef1f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
font-size: 0.9rem !important;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary */
|
||||||
|
.summary-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#total-billing {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #0f3460;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
@@ -113,13 +347,17 @@ header {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s, transform 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background: #27ae60;
|
background: #27ae60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -151,120 +389,6 @@ header {
|
|||||||
background: #c0392b;
|
background: #c0392b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search */
|
|
||||||
.search-bar {
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 2px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: #fff;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #0f3460;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table */
|
|
||||||
.table-container {
|
|
||||||
padding: 0 2rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
background: #16213e;
|
|
||||||
color: #fff;
|
|
||||||
padding: 14px 16px;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:hover {
|
|
||||||
background: #1a2744;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr {
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:hover {
|
|
||||||
background: #f7f9fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td {
|
|
||||||
padding: 12px 16px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-cell {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0f3460;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell button:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 3rem;
|
|
||||||
color: #888;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Summary */
|
|
||||||
.summary-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.25rem 2rem;
|
|
||||||
margin: 1rem 2rem 2rem;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
#total-billing {
|
|
||||||
font-size: 1.15rem;
|
|
||||||
color: #0f3460;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal */
|
/* Modal */
|
||||||
.modal {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -278,6 +402,7 @@ tbody td {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@@ -288,6 +413,12 @@ tbody td {
|
|||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
animation: modalIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalIn {
|
||||||
|
from { opacity: 0; transform: scale(0.95) translateY(10px); }
|
||||||
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
@@ -310,10 +441,18 @@ tbody td {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #999;
|
color: #999;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close:hover {
|
.modal-close:hover {
|
||||||
color: #333;
|
color: #333;
|
||||||
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form */
|
/* Form */
|
||||||
@@ -322,7 +461,7 @@ form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form h3 {
|
form h3 {
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
color: #0f3460;
|
color: #0f3460;
|
||||||
margin: 1.25rem 0 0.75rem;
|
margin: 1.25rem 0 0.75rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
@@ -349,7 +488,7 @@ form h3:first-child {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #555;
|
color: #555;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
@@ -360,8 +499,8 @@ form h3:first-child {
|
|||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid #e0e0e0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.93rem;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,6 +508,7 @@ form h3:first-child {
|
|||||||
.form-group textarea:focus {
|
.form-group textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #0f3460;
|
border-color: #0f3460;
|
||||||
|
box-shadow: 0 0 0 3px rgba(15,52,96,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
@@ -398,8 +538,12 @@ form h3:first-child {
|
|||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-section h3 {
|
.detail-section h3 {
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
color: #0f3460;
|
color: #0f3460;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
@@ -409,18 +553,19 @@ form h3:first-child {
|
|||||||
.detail-grid {
|
.detail-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 0.5rem 2rem;
|
gap: 0.75rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-item {
|
.detail-item {
|
||||||
padding: 0.4rem 0;
|
padding: 0.3rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-label {
|
.detail-label {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
color: #888;
|
color: #999;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-value {
|
.detail-value {
|
||||||
@@ -430,6 +575,15 @@ form h3:first-child {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-value a {
|
||||||
|
color: #0f3460;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-value.empty {
|
.detail-value.empty {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -439,10 +593,39 @@ form h3:first-child {
|
|||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
header {
|
header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-brand {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.form-grid,
|
.form-grid,
|
||||||
.detail-grid {
|
.detail-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -450,25 +633,26 @@ form h3:first-child {
|
|||||||
|
|
||||||
.summary-bar {
|
.summary-bar {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.25rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-bar {
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th,
|
thead th,
|
||||||
tbody td {
|
tbody td {
|
||||||
padding: 10px 8px;
|
padding: 10px 10px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header-right {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right .btn-primary,
|
||||||
|
.header-right .btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user