Dokumentit: drag & drop -tiedostolataus useille tiedostoille
- 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 <noreply@anthropic.com>
This commit is contained in:
16
index.html
16
index.html
@@ -741,9 +741,23 @@
|
|||||||
<div id="no-docs" class="empty-state" style="display:none;">
|
<div id="no-docs" class="empty-state" style="display:none;">
|
||||||
<div class="empty-icon">📄</div>
|
<div class="empty-icon">📄</div>
|
||||||
<p>Ei dokumentteja vielä.</p>
|
<p>Ei dokumentteja vielä.</p>
|
||||||
<p class="empty-hint">Klikkaa "+ Uusi dokumentti" aloittaaksesi.</p>
|
<p class="empty-hint">Klikkaa "+ Uusi dokumentti" tai raahaa tiedostoja alle.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Drag & Drop -tiedostolataus -->
|
||||||
|
<div id="doc-dropzone" class="doc-dropzone">
|
||||||
|
<div class="doc-dropzone-icon">📁</div>
|
||||||
|
<p>Raahaa tiedostoja tähän ladataksesi</p>
|
||||||
|
<p class="doc-dropzone-hint">tai <label for="doc-multi-file" class="doc-dropzone-link">valitse tiedostot</label></p>
|
||||||
|
<input type="file" id="doc-multi-file" multiple style="display:none;">
|
||||||
|
</div>
|
||||||
|
<div id="doc-upload-progress" class="doc-upload-progress" style="display:none;">
|
||||||
|
<div class="doc-upload-progress-bar">
|
||||||
|
<div class="doc-upload-progress-fill" id="doc-upload-fill"></div>
|
||||||
|
</div>
|
||||||
|
<span class="doc-upload-status" id="doc-upload-status">Ladataan...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lukunäkymä -->
|
<!-- Lukunäkymä -->
|
||||||
|
|||||||
195
script.js
195
script.js
@@ -5563,6 +5563,201 @@ document.getElementById('doc-edit-form')?.addEventListener('submit', async (e) =
|
|||||||
} catch (e) { alert('Tallennus epäonnistui: ' + e.message); }
|
} 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 ====================
|
// ==================== LAITETILAT ====================
|
||||||
|
|
||||||
let allLaitetilat = [];
|
let allLaitetilat = [];
|
||||||
|
|||||||
64
style.css
64
style.css
@@ -1681,6 +1681,70 @@ span.empty {
|
|||||||
color: #888;
|
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 */
|
/* Asiakasprofiilin dokumentit */
|
||||||
.customer-doc-item {
|
.customer-doc-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user