diff --git a/index.html b/index.html
index 7a6cb61..b1f20a8 100644
--- a/index.html
+++ b/index.html
@@ -741,9 +741,23 @@
đ
Ei dokumentteja vielä.
-
Klikkaa "+ Uusi dokumentti" aloittaaksesi.
+
Klikkaa "+ Uusi dokumentti" tai raahaa tiedostoja alle.
+
+
+
+
đ
+
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;