Add file uploads and speed distribution chart
- File upload/download/delete per customer (max 20MB, stored in data/files/) - Files section shown in customer detail modal - Speed distribution chart replaces single "top speed" stat - Bar chart shows all speeds with count, top speed bolded - Customer delete also cleans up associated files - data/files/ added to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
93
script.js
93
script.js
@@ -170,17 +170,25 @@ function updateTrivia() {
|
||||
setTrivia('stat-top-zip', '-', 'ei postinumeroita');
|
||||
}
|
||||
|
||||
// Suosituin nopeus
|
||||
// Nopeus-jakauma
|
||||
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', '-', '');
|
||||
const speedTable = document.getElementById('stat-speed-table');
|
||||
if (speedTable) {
|
||||
const sorted = Object.entries(speedCounts).sort((a, b) => b[1] - a[1]);
|
||||
const maxCount = sorted.length > 0 ? sorted[0][1] : 0;
|
||||
if (sorted.length === 0) {
|
||||
speedTable.innerHTML = '<span style="color:#aaa; font-size:0.85rem;">-</span>';
|
||||
} else {
|
||||
speedTable.innerHTML = sorted.map(([speed, cnt]) => {
|
||||
const isTop = cnt === maxCount;
|
||||
const barWidth = Math.max(20, (cnt / maxCount) * 60);
|
||||
return `<span class="speed-item ${isTop ? 'top' : ''}">${esc(speed)} (${cnt})<span class="speed-bar" style="width:${barWidth}px"></span></span>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Keskihinta
|
||||
@@ -325,9 +333,82 @@ function showDetail(id) {
|
||||
<h3>Lisätiedot</h3>
|
||||
<p style="white-space:pre-wrap; color:#555;">${esc(c.lisatiedot)}</p>
|
||||
</div>` : ''}
|
||||
<div class="detail-section">
|
||||
<h3>Tiedostot</h3>
|
||||
<div class="file-upload-area">
|
||||
<label class="file-upload-btn btn-primary" style="display:inline-block; cursor:pointer; font-size:0.85rem; padding:8px 16px;">
|
||||
+ Lisää tiedosto
|
||||
<input type="file" id="file-upload-input" style="display:none" multiple>
|
||||
</label>
|
||||
<span class="file-upload-hint" style="font-size:0.8rem; color:#999; margin-left:8px;">Max 20 MB / tiedosto</span>
|
||||
</div>
|
||||
<div id="file-list" class="file-list" style="margin-top:0.75rem;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
detailModal.style.display = 'flex';
|
||||
|
||||
// Lataa tiedostolista
|
||||
loadFiles(id);
|
||||
|
||||
// Upload handler
|
||||
const fileInput = document.getElementById('file-upload-input');
|
||||
fileInput.addEventListener('change', async () => {
|
||||
for (const file of fileInput.files) {
|
||||
const formData = new FormData();
|
||||
formData.append('customer_id', id);
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const res = await fetch(`${API}?action=file_upload`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData,
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) alert(data.error || 'Virhe');
|
||||
} catch (e) {
|
||||
alert('Tiedoston lähetys epäonnistui');
|
||||
}
|
||||
}
|
||||
fileInput.value = '';
|
||||
loadFiles(id);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadFiles(customerId) {
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) return;
|
||||
try {
|
||||
const files = await apiCall(`file_list&customer_id=${customerId}`);
|
||||
if (files.length === 0) {
|
||||
fileList.innerHTML = '<p style="color:#aaa; font-size:0.85rem;">Ei tiedostoja.</p>';
|
||||
return;
|
||||
}
|
||||
fileList.innerHTML = files.map(f => `
|
||||
<div class="file-item">
|
||||
<div class="file-info">
|
||||
<a href="${API}?action=file_download&customer_id=${customerId}&filename=${encodeURIComponent(f.filename)}"
|
||||
class="file-name" target="_blank">${esc(f.filename)}</a>
|
||||
<span class="file-meta">${formatFileSize(f.size)} · ${f.modified}</span>
|
||||
</div>
|
||||
<button class="file-delete-btn" onclick="deleteFile('${customerId}', '${esc(f.filename)}')" title="Poista">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
fileList.innerHTML = '<p style="color:#e74c3c; font-size:0.85rem;">Virhe ladattaessa tiedostoja.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
async function deleteFile(customerId, filename) {
|
||||
if (!confirm(`Poistetaanko tiedosto "${filename}"?`)) return;
|
||||
await apiCall('file_delete', 'POST', { customer_id: customerId, filename });
|
||||
loadFiles(customerId);
|
||||
}
|
||||
|
||||
// Detail modal actions
|
||||
|
||||
Reference in New Issue
Block a user