From c0b003c2f9da8fe6295a2e97c6409ac0dedf5ae1 Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Thu, 12 Mar 2026 11:24:04 +0200 Subject: [PATCH] Dokumentit: drag & drop -tiedostolataus useille tiedostoille MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop zone dokumenttilistan alaosaan (raahaa tai klikkaa) - Multi-file upload: luo dokumentit ja lataa tiedostot automaattisesti - Edistymispalkki näyttää latauksen tilanteen - Kansioiden raahaus: luo automaattisesti alikansio + tiedostot - Kategoria-tunnistus tiedostopäätteen mukaan (kuva/muu) - Multi-file input fallback perinteiselle tiedostovalinnalle Co-Authored-By: Claude Opus 4.6 --- index.html | 16 ++++- script.js | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 64 ++++++++++++++++++ 3 files changed, 274 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 7a6cb61..b1f20a8 100644 --- a/index.html +++ b/index.html @@ -741,9 +741,23 @@ + + +
+
📁
+

Raahaa tiedostoja tähän ladataksesi

+

tai

+ +
+ diff --git a/script.js b/script.js index a34d2c2..32674fc 100644 --- a/script.js +++ b/script.js @@ -5563,6 +5563,201 @@ document.getElementById('doc-edit-form')?.addEventListener('submit', async (e) = } catch (e) { alert('Tallennus epäonnistui: ' + e.message); } }); +// ---- Drag & Drop multi-upload ---- + +function detectDocCategory(filename) { + const ext = (filename.split('.').pop() || '').toLowerCase(); + if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp'].includes(ext)) return 'kuva'; + return 'muu'; +} + +// Käy läpi kansiorakenne webkitGetAsEntry:n avulla +function traverseFileTree(entry, path) { + return new Promise((resolve) => { + if (entry.isFile) { + entry.file(file => { + file._relativePath = path + file.name; + resolve([file]); + }); + } else if (entry.isDirectory) { + const reader = entry.createReader(); + reader.readEntries(async (entries) => { + let files = []; + for (const e of entries) { + const sub = await traverseFileTree(e, path + entry.name + '/'); + files = files.concat(sub); + } + // Merkitään kansion nimi + if (files.length > 0) { + files._folderName = entry.name; + } + resolve(files); + }); + } else { + resolve([]); + } + }); +} + +async function handleDocFileDrop(dataTransfer) { + const progressEl = document.getElementById('doc-upload-progress'); + const fillEl = document.getElementById('doc-upload-fill'); + const statusEl = document.getElementById('doc-upload-status'); + + // Kerää tiedostot — tarkista kansiot webkitGetAsEntry:llä + let allFiles = []; + let folderName = null; + const items = dataTransfer.items; + + if (items && items.length > 0 && items[0].webkitGetAsEntry) { + for (let i = 0; i < items.length; i++) { + const entry = items[i].webkitGetAsEntry(); + if (entry) { + if (entry.isDirectory) { + folderName = entry.name; + const files = await traverseFileTree(entry, ''); + allFiles = allFiles.concat(files); + } else { + const files = await traverseFileTree(entry, ''); + allFiles = allFiles.concat(files); + } + } + } + } else { + // Fallback: tavallinen files-lista + allFiles = Array.from(dataTransfer.files || []); + } + + if (allFiles.length === 0) return; + + // Näytä edistymispalkki + progressEl.style.display = ''; + fillEl.style.width = '0%'; + statusEl.textContent = `Ladataan 0 / ${allFiles.length} tiedostoa...`; + + // Jos raahattiin kansio → luo kansio ensin + let targetFolderId = currentDocFolderId || null; + if (folderName) { + try { + const folder = await apiCall('document_folder_save', 'POST', { + name: folderName, + parent_id: currentDocFolderId || null, + customer_id: currentDocCustomerId || null + }); + targetFolderId = folder.id; + } catch (e) { + console.error('Kansion luonti epäonnistui:', e); + } + } + + let success = 0; + let failed = 0; + for (let i = 0; i < allFiles.length; i++) { + const file = allFiles[i]; + const filename = file.name; + const pct = Math.round(((i) / allFiles.length) * 100); + fillEl.style.width = pct + '%'; + statusEl.textContent = `Ladataan ${i + 1} / ${allFiles.length}: ${filename}`; + + try { + // 1. Luo dokumentti + const saved = await apiCall('document_save', 'POST', { + title: filename.replace(/\.[^.]+$/, ''), + category: detectDocCategory(filename), + customer_id: currentDocCustomerId || null, + folder_id: targetFolderId, + max_versions: 10, + created_by: currentUser?.username || '' + }); + + // 2. Lataa tiedosto + const fd = new FormData(); + fd.append('document_id', saved.id); + fd.append('file', file); + fd.append('change_notes', 'Ensimmäinen versio'); + const res = await fetch(`${API}?action=document_upload`, { + method: 'POST', + credentials: 'include', + body: fd + }); + if (!res.ok) { + const errData = await res.json().catch(() => ({})); + throw new Error(errData.error || 'Upload failed'); + } + success++; + } catch (e) { + console.error(`Tiedoston "${filename}" lataus epäonnistui:`, e); + failed++; + } + } + + // Valmis + fillEl.style.width = '100%'; + const failText = failed > 0 ? ` (${failed} epäonnistui)` : ''; + statusEl.textContent = `✓ ${success} tiedostoa ladattu${failText}`; + + // Päivitä lista + await loadDocuments(); + if (folderName && targetFolderId) { + navigateDocFolder(targetFolderId); + } + + // Piilota edistymispalkki hetken kuluttua + setTimeout(() => { + progressEl.style.display = 'none'; + fillEl.style.width = '0%'; + }, 3000); +} + +// Drop zone handlerit +const docDropzone = document.getElementById('doc-dropzone'); +if (docDropzone) { + let dragCounter = 0; + + docDropzone.addEventListener('dragenter', (e) => { + e.preventDefault(); + dragCounter++; + docDropzone.classList.add('active'); + }); + + docDropzone.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + }); + + docDropzone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dragCounter--; + if (dragCounter <= 0) { + dragCounter = 0; + docDropzone.classList.remove('active'); + } + }); + + docDropzone.addEventListener('drop', async (e) => { + e.preventDefault(); + dragCounter = 0; + docDropzone.classList.remove('active'); + await handleDocFileDrop(e.dataTransfer); + }); + + // Klikkaa drop zonea → avaa file dialog + docDropzone.addEventListener('click', (e) => { + if (e.target.tagName !== 'LABEL' && e.target.tagName !== 'INPUT') { + document.getElementById('doc-multi-file')?.click(); + } + }); +} + +// Multi-file input fallback +document.getElementById('doc-multi-file')?.addEventListener('change', async (e) => { + if (e.target.files.length > 0) { + // Luo pseudo-DataTransfer jossa on files + await handleDocFileDrop({ files: e.target.files, items: null }); + e.target.value = ''; + } +}); + // ==================== LAITETILAT ==================== let allLaitetilat = []; diff --git a/style.css b/style.css index b668b11..d8cfd1c 100644 --- a/style.css +++ b/style.css @@ -1681,6 +1681,70 @@ span.empty { color: #888; } +/* Drag & Drop -tiedostolataus */ +.doc-dropzone { + border: 2px dashed #d1d5db; + border-radius: 12px; + padding: 2rem 1rem; + text-align: center; + transition: all 0.2s; + margin-top: 1rem; + cursor: pointer; + background: #fafafa; +} +.doc-dropzone:hover { + border-color: #a0aec0; + background: #f7fafc; +} +.doc-dropzone.active { + border-color: var(--primary-color); + background: #eff6ff; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} +.doc-dropzone-icon { + font-size: 2rem; + margin-bottom: 0.5rem; +} +.doc-dropzone p { + margin: 0.25rem 0; + color: #666; + font-size: 0.9rem; +} +.doc-dropzone-hint { + font-size: 0.85rem !important; + color: #999 !important; +} +.doc-dropzone-link { + color: var(--primary-color); + cursor: pointer; + text-decoration: underline; +} +.doc-upload-progress { + margin-top: 0.75rem; + padding: 0.75rem 1rem; + background: #f8fafc; + border-radius: 8px; + border: 1px solid #e2e8f0; +} +.doc-upload-progress-bar { + height: 6px; + background: #e5e7eb; + border-radius: 3px; + overflow: hidden; + margin-bottom: 0.4rem; +} +.doc-upload-progress-fill { + height: 100%; + background: var(--primary-color); + transition: width 0.3s; + width: 0%; + border-radius: 3px; +} +.doc-upload-status { + font-size: 0.8rem; + color: #666; +} + /* Asiakasprofiilin dokumentit */ .customer-doc-item { display: flex;