Add CuituNet Intra customer management CMS
Password-protected intranet for managing fiber internet customers: - Customer table (company, address, speed, price) - Click row to view full details (contact & billing info) - Add, edit, delete customers - Search and sortable columns - Total billing summary - PHP + vanilla JS + JSON storage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
337
script.js
Normal file
337
script.js
Normal file
@@ -0,0 +1,337 @@
|
||||
const API = 'api.php';
|
||||
let customers = [];
|
||||
let sortField = 'yritys';
|
||||
let sortAsc = true;
|
||||
let currentDetailId = null;
|
||||
|
||||
// Elements
|
||||
const loginScreen = document.getElementById('login-screen');
|
||||
const dashboard = document.getElementById('dashboard');
|
||||
const loginForm = document.getElementById('login-form');
|
||||
const loginError = document.getElementById('login-error');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const tbody = document.getElementById('customer-tbody');
|
||||
const noCustomers = document.getElementById('no-customers');
|
||||
const customerCount = document.getElementById('customer-count');
|
||||
const totalBilling = document.getElementById('total-billing');
|
||||
const customerModal = document.getElementById('customer-modal');
|
||||
const detailModal = document.getElementById('detail-modal');
|
||||
const customerForm = document.getElementById('customer-form');
|
||||
|
||||
// API helpers
|
||||
async function apiCall(action, method = 'GET', body = null) {
|
||||
const opts = { method, credentials: 'include' };
|
||||
if (body) {
|
||||
opts.headers = { 'Content-Type': 'application/json' };
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const res = await fetch(`${API}?action=${action}`, opts);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Virhe');
|
||||
return data;
|
||||
}
|
||||
|
||||
// Auth
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const data = await apiCall('check_auth');
|
||||
if (data.authenticated) {
|
||||
showDashboard();
|
||||
}
|
||||
} catch (e) { /* not logged in */ }
|
||||
}
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const password = document.getElementById('login-password').value;
|
||||
try {
|
||||
await apiCall('login', 'POST', { password });
|
||||
loginError.style.display = 'none';
|
||||
showDashboard();
|
||||
} catch (err) {
|
||||
loginError.textContent = err.message;
|
||||
loginError.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-logout').addEventListener('click', async () => {
|
||||
await apiCall('logout');
|
||||
dashboard.style.display = 'none';
|
||||
loginScreen.style.display = 'flex';
|
||||
document.getElementById('login-password').value = '';
|
||||
});
|
||||
|
||||
async function showDashboard() {
|
||||
loginScreen.style.display = 'none';
|
||||
dashboard.style.display = 'block';
|
||||
await loadCustomers();
|
||||
}
|
||||
|
||||
// Customers
|
||||
async function loadCustomers() {
|
||||
customers = await apiCall('customers');
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const query = searchInput.value.toLowerCase().trim();
|
||||
let filtered = customers;
|
||||
if (query) {
|
||||
filtered = customers.filter(c =>
|
||||
c.yritys.toLowerCase().includes(query) ||
|
||||
c.asennusosoite.toLowerCase().includes(query) ||
|
||||
(c.yhteyshenkilö && c.yhteyshenkilö.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
// Sort
|
||||
filtered.sort((a, b) => {
|
||||
let va = a[sortField] ?? '';
|
||||
let vb = b[sortField] ?? '';
|
||||
if (sortField === 'hinta') {
|
||||
va = parseFloat(va) || 0;
|
||||
vb = parseFloat(vb) || 0;
|
||||
} else {
|
||||
va = String(va).toLowerCase();
|
||||
vb = String(vb).toLowerCase();
|
||||
}
|
||||
if (va < vb) return sortAsc ? -1 : 1;
|
||||
if (va > vb) return sortAsc ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
noCustomers.style.display = 'block';
|
||||
document.getElementById('customer-table').style.display = 'none';
|
||||
} else {
|
||||
noCustomers.style.display = 'none';
|
||||
document.getElementById('customer-table').style.display = 'table';
|
||||
tbody.innerHTML = filtered.map(c => `
|
||||
<tr data-id="${c.id}">
|
||||
<td>${esc(c.yritys)}</td>
|
||||
<td>${esc(c.asennusosoite)}</td>
|
||||
<td>${esc(c.liittymanopeus)}</td>
|
||||
<td class="price-cell">${formatPrice(c.hinta)}</td>
|
||||
<td class="actions-cell">
|
||||
<button onclick="event.stopPropagation(); editCustomer('${c.id}')" title="Muokkaa">✏️</button>
|
||||
<button onclick="event.stopPropagation(); deleteCustomer('${c.id}', '${esc(c.yritys)}')" title="Poista">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
updateSummary(filtered);
|
||||
}
|
||||
|
||||
function updateSummary(filtered) {
|
||||
const count = customers.length;
|
||||
const total = customers.reduce((sum, c) => sum + (parseFloat(c.hinta) || 0), 0);
|
||||
customerCount.textContent = `${count} asiakasta`;
|
||||
totalBilling.textContent = `Laskutus yhteensä: ${formatPrice(total)}/kk`;
|
||||
}
|
||||
|
||||
function formatPrice(val) {
|
||||
return parseFloat(val || 0).toFixed(2).replace('.', ',') + ' €';
|
||||
}
|
||||
|
||||
function esc(str) {
|
||||
if (!str) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Search
|
||||
searchInput.addEventListener('input', () => renderTable());
|
||||
|
||||
// Sort
|
||||
document.querySelectorAll('th[data-sort]').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
const field = th.dataset.sort;
|
||||
if (sortField === field) {
|
||||
sortAsc = !sortAsc;
|
||||
} else {
|
||||
sortField = field;
|
||||
sortAsc = true;
|
||||
}
|
||||
renderTable();
|
||||
});
|
||||
});
|
||||
|
||||
// Row click -> detail
|
||||
tbody.addEventListener('click', (e) => {
|
||||
const row = e.target.closest('tr');
|
||||
if (!row) return;
|
||||
const id = row.dataset.id;
|
||||
showDetail(id);
|
||||
});
|
||||
|
||||
function showDetail(id) {
|
||||
const c = customers.find(x => x.id === id);
|
||||
if (!c) return;
|
||||
currentDetailId = id;
|
||||
|
||||
document.getElementById('detail-title').textContent = c.yritys;
|
||||
document.getElementById('detail-body').innerHTML = `
|
||||
<div class="detail-section">
|
||||
<h3>Liittymätiedot</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>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Y-tunnus</div>
|
||||
<div class="detail-value">${esc(c.ytunnus) || '<span class="empty">-</span>'}</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>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Nopeus</div>
|
||||
<div class="detail-value">${esc(c.liittymanopeus) || '<span class="empty">-</span>'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Hinta</div>
|
||||
<div class="detail-value price-cell">${formatPrice(c.hinta)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h3>Yhteystiedot</h3>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h3>Laskutustiedot</h3>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
${c.lisatiedot ? `
|
||||
<div class="detail-section">
|
||||
<h3>Lisätiedot</h3>
|
||||
<p style="white-space:pre-wrap; color:#555;">${esc(c.lisatiedot)}</p>
|
||||
</div>` : ''}
|
||||
`;
|
||||
|
||||
detailModal.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Detail modal actions
|
||||
document.getElementById('detail-close').addEventListener('click', () => detailModal.style.display = 'none');
|
||||
document.getElementById('detail-cancel').addEventListener('click', () => detailModal.style.display = 'none');
|
||||
document.getElementById('detail-edit').addEventListener('click', () => {
|
||||
detailModal.style.display = 'none';
|
||||
editCustomer(currentDetailId);
|
||||
});
|
||||
document.getElementById('detail-delete').addEventListener('click', () => {
|
||||
const c = customers.find(x => x.id === currentDetailId);
|
||||
if (c) {
|
||||
detailModal.style.display = 'none';
|
||||
deleteCustomer(currentDetailId, c.yritys);
|
||||
}
|
||||
});
|
||||
|
||||
// Add/Edit modal
|
||||
document.getElementById('btn-add').addEventListener('click', () => openCustomerForm());
|
||||
document.getElementById('modal-close').addEventListener('click', () => customerModal.style.display = 'none');
|
||||
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 : '';
|
||||
customerModal.style.display = 'flex';
|
||||
document.getElementById('form-yritys').focus();
|
||||
}
|
||||
|
||||
function editCustomer(id) {
|
||||
const c = customers.find(x => x.id === id);
|
||||
if (c) openCustomerForm(c);
|
||||
}
|
||||
|
||||
async function deleteCustomer(id, name) {
|
||||
if (!confirm(`Poistetaanko asiakas "${name}"?`)) return;
|
||||
await apiCall('customer_delete', 'POST', { id });
|
||||
await loadCustomers();
|
||||
}
|
||||
|
||||
customerForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('form-id').value;
|
||||
const data = {
|
||||
yritys: document.getElementById('form-yritys').value,
|
||||
ytunnus: document.getElementById('form-ytunnus').value,
|
||||
asennusosoite: document.getElementById('form-asennusosoite').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,
|
||||
laskutussahkoposti: document.getElementById('form-laskutussahkoposti').value,
|
||||
lisatiedot: document.getElementById('form-lisatiedot').value,
|
||||
};
|
||||
|
||||
if (id) {
|
||||
data.id = id;
|
||||
await apiCall('customer_update', 'POST', data);
|
||||
} else {
|
||||
await apiCall('customer', 'POST', data);
|
||||
}
|
||||
|
||||
customerModal.style.display = 'none';
|
||||
await loadCustomers();
|
||||
});
|
||||
|
||||
// Close modals on backdrop click
|
||||
customerModal.addEventListener('click', (e) => {
|
||||
if (e.target === customerModal) customerModal.style.display = 'none';
|
||||
});
|
||||
detailModal.addEventListener('click', (e) => {
|
||||
if (e.target === detailModal) detailModal.style.display = 'none';
|
||||
});
|
||||
|
||||
// ESC to close modals
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
customerModal.style.display = 'none';
|
||||
detailModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Init
|
||||
checkAuth();
|
||||
Reference in New Issue
Block a user