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:
160
script.js
160
script.js
@@ -79,8 +79,11 @@ function renderTable() {
|
||||
if (query) {
|
||||
filtered = customers.filter(c =>
|
||||
c.yritys.toLowerCase().includes(query) ||
|
||||
c.asennusosoite.toLowerCase().includes(query) ||
|
||||
(c.yhteyshenkilö && c.yhteyshenkilö.toLowerCase().includes(query))
|
||||
(c.asennusosoite || '').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';
|
||||
tbody.innerHTML = filtered.map(c => `
|
||||
<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.postinumero)}</td>
|
||||
<td>${esc(c.kaupunki)}</td>
|
||||
<td>${esc(c.liittymanopeus)}</td>
|
||||
<td class="price-cell">${formatPrice(c.hinta)}</td>
|
||||
<td class="actions-cell">
|
||||
@@ -129,6 +134,70 @@ function updateSummary(filtered) {
|
||||
const total = customers.reduce((sum, c) => sum + (parseFloat(c.hinta) || 0), 0);
|
||||
customerCount.textContent = `${count} asiakasta`;
|
||||
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) {
|
||||
@@ -167,34 +236,48 @@ tbody.addEventListener('click', (e) => {
|
||||
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) {
|
||||
const c = customers.find(x => x.id === id);
|
||||
if (!c) return;
|
||||
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-body').innerHTML = `
|
||||
<div class="detail-section">
|
||||
<h3>Liittymätiedot</h3>
|
||||
<h3>Yritys ja liittymä</h3>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<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 class="detail-item">
|
||||
<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 class="detail-item">
|
||||
<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 class="detail-item">
|
||||
<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 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>
|
||||
</div>
|
||||
@@ -204,15 +287,15 @@ function showDetail(id) {
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<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 class="detail-item">
|
||||
<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 class="detail-item">
|
||||
<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>
|
||||
@@ -221,11 +304,19 @@ function showDetail(id) {
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<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 class="detail-item">
|
||||
<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>
|
||||
@@ -260,20 +351,27 @@ document.getElementById('modal-close').addEventListener('click', () => customerM
|
||||
document.getElementById('form-cancel').addEventListener('click', () => customerModal.style.display = 'none');
|
||||
|
||||
function openCustomerForm(customer = null) {
|
||||
document.getElementById('modal-title').textContent = customer ? 'Muokkaa asiakasta' : 'Lisää asiakas';
|
||||
document.getElementById('form-submit').textContent = customer ? 'Päivitä' : 'Tallenna';
|
||||
document.getElementById('form-id').value = customer ? customer.id : '';
|
||||
document.getElementById('form-yritys').value = customer ? customer.yritys : '';
|
||||
document.getElementById('form-ytunnus').value = customer ? customer.ytunnus : '';
|
||||
document.getElementById('form-asennusosoite').value = customer ? customer.asennusosoite : '';
|
||||
document.getElementById('form-liittymanopeus').value = customer ? customer.liittymanopeus : '';
|
||||
document.getElementById('form-hinta').value = customer ? customer.hinta : '';
|
||||
document.getElementById('form-yhteyshenkilo').value = customer ? customer.yhteyshenkilö : '';
|
||||
document.getElementById('form-puhelin').value = customer ? customer.puhelin : '';
|
||||
document.getElementById('form-sahkoposti').value = customer ? customer.sahkoposti : '';
|
||||
document.getElementById('form-laskutusosoite').value = customer ? customer.laskutusosoite : '';
|
||||
document.getElementById('form-laskutussahkoposti').value = customer ? customer.laskutussahkoposti : '';
|
||||
document.getElementById('form-lisatiedot').value = customer ? customer.lisatiedot : '';
|
||||
const c = customer;
|
||||
document.getElementById('modal-title').textContent = c ? 'Muokkaa asiakasta' : 'Lisää asiakas';
|
||||
document.getElementById('form-submit').textContent = c ? 'Päivitä' : 'Tallenna';
|
||||
document.getElementById('form-id').value = c ? c.id : '';
|
||||
document.getElementById('form-yritys').value = c ? c.yritys : '';
|
||||
document.getElementById('form-ytunnus').value = c ? c.ytunnus : '';
|
||||
document.getElementById('form-asennusosoite').value = c ? c.asennusosoite : '';
|
||||
document.getElementById('form-postinumero').value = c ? (c.postinumero || '') : '';
|
||||
document.getElementById('form-kaupunki').value = c ? (c.kaupunki || '') : '';
|
||||
document.getElementById('form-liittymanopeus').value = c ? c.liittymanopeus : '';
|
||||
document.getElementById('form-hinta').value = c ? c.hinta : '';
|
||||
document.getElementById('form-yhteyshenkilo').value = c ? c.yhteyshenkilö : '';
|
||||
document.getElementById('form-puhelin').value = c ? c.puhelin : '';
|
||||
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';
|
||||
document.getElementById('form-yritys').focus();
|
||||
}
|
||||
@@ -296,13 +394,19 @@ customerForm.addEventListener('submit', async (e) => {
|
||||
yritys: document.getElementById('form-yritys').value,
|
||||
ytunnus: document.getElementById('form-ytunnus').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,
|
||||
hinta: document.getElementById('form-hinta').value,
|
||||
yhteyshenkilö: document.getElementById('form-yhteyshenkilo').value,
|
||||
puhelin: document.getElementById('form-puhelin').value,
|
||||
sahkoposti: document.getElementById('form-sahkoposti').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,
|
||||
elaskuosoite: document.getElementById('form-elaskuosoite').value,
|
||||
elaskuvalittaja: document.getElementById('form-elaskuvalittaja').value,
|
||||
lisatiedot: document.getElementById('form-lisatiedot').value,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user