From f40b387383f2aae1f6a613bf613c8c42a2cf9464 Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Wed, 11 Mar 2026 23:19:05 +0200 Subject: [PATCH] Dokumentit: kokousmuistiot, kansiorakenne, sub-tabit, asiakaslinkkaus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Uusi dokumenttityyppi "kokousmuistio" jolla inline tekstieditori (ei tiedostopohjainen) - document_versions.content -sarake kokousmuistioiden tekstin tallennukseen - Sub-tabit Dokumentit-välilehdelle (Kaikki / Kokoukset) Tekniikka-mallin mukaan - Kansiorakenne: document_folders-taulu, kansionavigaatio breadcrumbsilla - Uudet API-endpointit: document_folders, document_folder_save/delete, document_content_save, document_move - Asiakasprofiilin Dokumentit-osio: näyttää linkitetyt dokumentit + pikanapit luontiin - Asiakasprofiilista voi avata dokumentin suoraan tai luoda uuden linkitettynä asiakkaaseen Co-Authored-By: Claude Opus 4.6 --- api.php | 84 ++++++++++++++ db.php | 63 +++++++++- index.html | 59 ++++++++-- script.js | 335 ++++++++++++++++++++++++++++++++++++++++++++++++----- style.css | 46 ++++++++ 5 files changed, 549 insertions(+), 38 deletions(-) diff --git a/api.php b/api.php index 98525d6..45d6596 100644 --- a/api.php +++ b/api.php @@ -4310,6 +4310,90 @@ switch ($action) { } break; + // ==================== DOKUMENTTIKANSIOT ==================== + + case 'document_folders': + requireAuth(); + $companyId = requireCompany(); + echo json_encode(dbLoadFolders($companyId)); + break; + + case 'document_folder_save': + requireAuth(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + if (empty($input['name'])) { + http_response_code(400); + echo json_encode(['error' => 'Kansion nimi puuttuu']); + break; + } + $input['created_by'] = currentUser(); + $id = dbSaveFolder($companyId, $input); + echo json_encode(['id' => $id, 'name' => $input['name']]); + break; + + case 'document_folder_delete': + requireAdmin(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + $result = dbDeleteFolder($companyId, $input['id'] ?? ''); + echo json_encode(['ok' => $result]); + break; + + case 'document_content_save': + requireAuth(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + try { + $input = json_decode(file_get_contents('php://input'), true); + $docId = $input['document_id'] ?? ''; + $doc = dbLoadDocument($docId); + if (!$doc || $doc['company_id'] !== $companyId) { + http_response_code(404); + echo json_encode(['error' => 'Dokumenttia ei löytynyt']); + break; + } + dbAddDocumentVersion($docId, [ + 'content' => $input['content'] ?? '', + 'change_notes' => $input['change_notes'] ?? '', + 'created_by' => currentUser(), + 'filename' => '', + 'original_name' => '', + 'file_size' => strlen($input['content'] ?? ''), + 'mime_type' => 'text/plain' + ]); + $updated = dbLoadDocument($docId); + echo json_encode($updated); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => 'Tallennus epäonnistui: ' . $e->getMessage()]); + } + break; + + case 'document_move': + requireAuth(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + try { + $input = json_decode(file_get_contents('php://input'), true); + $docId = $input['document_id'] ?? ''; + $folderId = $input['folder_id'] ?? null; + $doc = dbLoadDocument($docId); + if (!$doc || $doc['company_id'] !== $companyId) { + http_response_code(404); + echo json_encode(['error' => 'Dokumenttia ei löytynyt']); + break; + } + _dbExecute("UPDATE documents SET folder_id = ? WHERE id = ?", [$folderId ?: null, $docId]); + echo json_encode(['ok' => true]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => 'Siirto epäonnistui: ' . $e->getMessage()]); + } + break; + // ==================== NETADMIN ==================== case 'netadmin_connections': diff --git a/db.php b/db.php index c6abb9d..e384968 100644 --- a/db.php +++ b/db.php @@ -542,6 +542,18 @@ function initDatabase(): void { INDEX idx_document (document_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + "CREATE TABLE IF NOT EXISTS document_folders ( + id VARCHAR(20) PRIMARY KEY, + company_id VARCHAR(50) NOT NULL, + name VARCHAR(255) NOT NULL, + parent_id VARCHAR(20) DEFAULT NULL, + created_by VARCHAR(100) DEFAULT '', + luotu DATETIME, + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + INDEX idx_company (company_id), + INDEX idx_parent (parent_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + "CREATE TABLE IF NOT EXISTS laitetilat ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, @@ -599,6 +611,8 @@ function initDatabase(): void { "ALTER TABLE companies ADD COLUMN allowed_ips TEXT DEFAULT '' AFTER enabled_modules", "ALTER TABLE todos ADD COLUMN category VARCHAR(30) DEFAULT '' AFTER priority", "ALTER TABLE user_companies ADD COLUMN role VARCHAR(20) DEFAULT 'user' AFTER company_id", + "ALTER TABLE documents ADD COLUMN folder_id VARCHAR(20) DEFAULT NULL AFTER customer_id", + "ALTER TABLE document_versions ADD COLUMN content MEDIUMTEXT DEFAULT NULL AFTER mime_type", ]; foreach ($alters as $sql) { try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } @@ -1736,6 +1750,43 @@ function dbDeleteTodoSubtask(string $subtaskId): void { _dbExecute("DELETE FROM todo_subtasks WHERE id = ?", [$subtaskId]); } +// ==================== DOKUMENTTIKANSIOT ==================== + +function dbLoadFolders(string $companyId): array { + return _dbFetchAll("SELECT * FROM document_folders WHERE company_id = ? ORDER BY name", [$companyId]); +} + +function dbSaveFolder(string $companyId, array $folder): string { + $id = $folder['id'] ?? generateId(); + $now = date('Y-m-d H:i:s'); + _dbExecute(" + INSERT INTO document_folders (id, company_id, name, parent_id, created_by, luotu) + VALUES (:id, :companyId, :name, :parentId, :createdBy, :luotu) + ON DUPLICATE KEY UPDATE name = VALUES(name), parent_id = VALUES(parent_id) + ", [ + 'id' => $id, + 'companyId' => $companyId, + 'name' => $folder['name'] ?? '', + 'parentId' => !empty($folder['parent_id']) ? $folder['parent_id'] : null, + 'createdBy' => $folder['created_by'] ?? '', + 'luotu' => $folder['luotu'] ?? $now + ]); + return $id; +} + +function dbDeleteFolder(string $companyId, string $folderId): bool { + $folder = _dbFetchOne("SELECT parent_id FROM document_folders WHERE id = ? AND company_id = ?", [$folderId, $companyId]); + if (!$folder) return false; + // Siirrä kansion dokumentit ylätasolle + _dbExecute("UPDATE documents SET folder_id = ? WHERE folder_id = ? AND company_id = ?", + [$folder['parent_id'], $folderId, $companyId]); + // Siirrä alikansiot ylätasolle + _dbExecute("UPDATE document_folders SET parent_id = ? WHERE parent_id = ? AND company_id = ?", + [$folder['parent_id'], $folderId, $companyId]); + _dbExecute("DELETE FROM document_folders WHERE id = ? AND company_id = ?", [$folderId, $companyId]); + return true; +} + // ==================== DOKUMENTIT ==================== function dbLoadDocuments(string $companyId, ?string $customerId = null): array { @@ -1767,19 +1818,21 @@ function dbSaveDocument(string $companyId, array $doc): string { $id = $doc['id'] ?? generateId(); $now = date('Y-m-d H:i:s'); _dbExecute(" - INSERT INTO documents (id, company_id, customer_id, title, description, category, current_version, created_by, luotu, muokattu, muokkaaja) - VALUES (:id, :companyId, :customerId, :title, :description, :category, :currentVersion, :createdBy, :luotu, :muokattu, :muokkaaja) + INSERT INTO documents (id, company_id, customer_id, folder_id, title, description, category, current_version, created_by, luotu, muokattu, muokkaaja) + VALUES (:id, :companyId, :customerId, :folderId, :title, :description, :category, :currentVersion, :createdBy, :luotu, :muokattu, :muokkaaja) ON DUPLICATE KEY UPDATE title = VALUES(title), description = VALUES(description), category = VALUES(category), customer_id = VALUES(customer_id), + folder_id = VALUES(folder_id), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) ", [ 'id' => $id, 'companyId' => $companyId, 'customerId' => !empty($doc['customer_id']) ? $doc['customer_id'] : null, + 'folderId' => !empty($doc['folder_id']) ? $doc['folder_id'] : null, 'title' => $doc['title'] ?? '', 'description' => $doc['description'] ?? '', 'category' => $doc['category'] ?? 'muu', @@ -1807,7 +1860,7 @@ function dbAddDocumentVersion(string $documentId, array $version): void { $maxVersion = _dbFetchScalar("SELECT COALESCE(MAX(version_number), 0) FROM document_versions WHERE document_id = ?", [$documentId]); $nextVersion = (int)$maxVersion + 1; - _dbExecute("INSERT INTO document_versions (id, document_id, version_number, filename, original_name, file_size, mime_type, change_notes, created_by, luotu) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ + _dbExecute("INSERT INTO document_versions (id, document_id, version_number, filename, original_name, file_size, mime_type, content, change_notes, created_by, luotu) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ $version['id'] ?? generateId(), $documentId, $nextVersion, @@ -1815,6 +1868,7 @@ function dbAddDocumentVersion(string $documentId, array $version): void { $version['original_name'] ?? '', $version['file_size'] ?? 0, $version['mime_type'] ?? '', + $version['content'] ?? null, $version['change_notes'] ?? '', $version['created_by'] ?? '', $now @@ -1836,7 +1890,7 @@ function dbRestoreDocumentVersion(string $documentId, string $versionId, string $nextVersion = (int)$maxVersion + 1; $newId = generateId(); - _dbExecute("INSERT INTO document_versions (id, document_id, version_number, filename, original_name, file_size, mime_type, change_notes, created_by, luotu) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ + _dbExecute("INSERT INTO document_versions (id, document_id, version_number, filename, original_name, file_size, mime_type, content, change_notes, created_by, luotu) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ $newId, $documentId, $nextVersion, @@ -1844,6 +1898,7 @@ function dbRestoreDocumentVersion(string $documentId, string $versionId, string $oldVersion['original_name'], $oldVersion['file_size'], $oldVersion['mime_type'], + $oldVersion['content'] ?? null, 'Palautettu versiosta ' . $oldVersion['version_number'], $user, $now diff --git a/index.html b/index.html index a191e37..880a3cb 100644 --- a/index.html +++ b/index.html @@ -730,13 +730,26 @@
+
+ + +
-

📄 Dokumentit

- +

📄 Dokumentit

+
+ + +
+ +
+ + +
+
@@ -792,13 +806,31 @@

- -
+ + + + + + + +
- -
+ +

Lataa uusi versio

@@ -848,6 +880,7 @@ +
@@ -861,11 +894,23 @@
-
+
+ + +
diff --git a/script.js b/script.js index 6cc09a0..b460fd0 100644 --- a/script.js +++ b/script.js @@ -284,7 +284,7 @@ function switchToTab(target, subTab) { if (target === 'ohjeet') loadGuides(); if (target === 'todo') { loadTodos(); if (subTab) switchTodoSubTab(subTab); } if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); } - if (target === 'documents') { loadDocuments(); showDocsListView(); } + if (target === 'documents') { loadDocuments(); showDocsListView(); if (subTab === 'kokoukset') switchDocSubTab('docs-kokoukset'); else switchDocSubTab('docs-all'); } if (target === 'laitetilat') { loadLaitetilat(); showLaitetilatListView(); } if (target === 'netadmin') loadNetadmin(); if (target === 'users') loadUsers(); @@ -597,6 +597,11 @@ function showDetail(id) { Max 20 MB / tiedosto
+
+

Dokumentit

+
Ladataan...
+ +
`; // Synkronoi prices-hidden tila detail-modaliin @@ -604,6 +609,7 @@ function showDetail(id) { detailModal.querySelector('.modal-content')?.classList.toggle('prices-hidden', !!pricesHidden); detailModal.style.display = 'flex'; loadFiles(id); + loadCustomerDocuments(id); document.getElementById('file-upload-input').addEventListener('change', async function () { for (const file of this.files) { const fd = new FormData(); @@ -620,6 +626,44 @@ function showDetail(id) { }); } +async function loadCustomerDocuments(customerId) { + const container = document.getElementById('customer-docs-list'); + if (!container) return; + try { + const docs = await apiCall(`documents&customer_id=${customerId}`); + if (docs.length === 0) { + container.innerHTML = '

Ei dokumentteja.

'; + return; + } + container.innerHTML = docs.map(d => { + const catLabel = docCategoryLabels[d.category] || d.category || 'Muu'; + const date = d.muokattu ? new Date(d.muokattu).toLocaleDateString('fi-FI') : ''; + return `
+ ${esc(d.title)} + ${catLabel} ${date} +
`; + }).join(''); + } catch (e) { + container.innerHTML = '

Virhe ladattaessa dokumentteja.

'; + } +} + +window.openDocFromCustomer = async function(docId) { + detailModal.style.display = 'none'; + switchToTab('documents'); + try { + currentDocument = await apiCall(`document&id=${docId}`); + renderDocReadView(); + showDocReadView(); + } catch (e) { alert('Dokumentin avaus epäonnistui: ' + e.message); } +}; + +window.openDocEditForCustomer = function(customerId, forceCategory) { + detailModal.style.display = 'none'; + switchToTab('documents'); + openDocEdit(null, forceCategory || null, customerId); +}; + async function loadFiles(customerId) { const fileList = document.getElementById('file-list'); if (!fileList) return; @@ -4688,12 +4732,16 @@ function openFeatureSuggestion() { let allDocuments = []; let currentDocument = null; +let allDocFolders = []; +let currentDocFolderId = null; // null = root (kaikki) +let docSubTabMode = 'docs-all'; // 'docs-all' | 'docs-kokoukset' const docCategoryLabels = { sopimus: 'Sopimus', lasku: 'Lasku', ohje: 'Ohje', raportti: 'Raportti', + kokousmuistio: 'Kokousmuistio', muu: 'Muu' }; @@ -4718,7 +4766,9 @@ function showDocEditView() { async function loadDocuments() { try { allDocuments = await apiCall('documents'); + try { allDocFolders = await apiCall('document_folders'); } catch (e2) { allDocFolders = []; } populateDocCustomerFilter(); + renderDocFolderBar(); renderDocumentsList(); } catch (e) { console.error('Dokumenttien lataus epäonnistui:', e); } } @@ -4747,12 +4797,107 @@ function populateDocCustomerFilter() { sel.value = existing || ''; } +// ---- Kansionavigointi ---- + +function renderDocFolderBar() { + const bc = document.getElementById('doc-breadcrumbs'); + if (!bc) return; + // Piilotetaan kansiot kokoukset-subtabissa + const showFolders = docSubTabMode !== 'docs-kokoukset'; + document.getElementById('doc-folder-bar').style.display = showFolders ? 'flex' : 'none'; + document.getElementById('doc-folders-grid').style.display = showFolders ? 'flex' : 'none'; + if (!showFolders) return; + + let crumbs = `📁 Kaikki`; + if (currentDocFolderId) { + const path = getDocFolderPath(currentDocFolderId); + path.forEach(f => { + crumbs += `/${esc(f.name)}`; + }); + } + bc.innerHTML = crumbs; + + // Alikansiot + const subfolders = allDocFolders.filter(f => (f.parent_id || null) === currentDocFolderId); + const grid = document.getElementById('doc-folders-grid'); + grid.innerHTML = subfolders.map(f => + `
📁 ${esc(f.name)}
` + ).join(''); +} + +function getDocFolderPath(folderId) { + const path = []; + let current = folderId; + let safety = 20; + while (current && safety-- > 0) { + const folder = allDocFolders.find(f => f.id === current); + if (!folder) break; + path.unshift(folder); + current = folder.parent_id || null; + } + return path; +} + +function navigateDocFolder(folderId) { + currentDocFolderId = folderId; + renderDocFolderBar(); + renderDocumentsList(); +} + +// ---- Sub-tabit ---- + +function switchDocSubTab(target) { + docSubTabMode = target; + document.querySelectorAll('#doc-sub-tab-bar .sub-tab').forEach(t => t.classList.remove('active')); + const btn = document.querySelector(`[data-doc-subtab="${target}"]`); + if (btn) btn.classList.add('active'); + + const isMeeting = target === 'docs-kokoukset'; + document.getElementById('btn-new-document').style.display = isMeeting ? 'none' : ''; + document.getElementById('btn-new-meeting-note').style.display = isMeeting ? '' : 'none'; + document.getElementById('docs-list-title').textContent = isMeeting ? '📝 Kokoukset' : '📄 Dokumentit'; + + // Nollaa kansionavigointi kokoukset-tilassa + if (isMeeting) currentDocFolderId = null; + + // Piilota suodattimet kokoukset-tilassa + document.getElementById('doc-filter-category').style.display = isMeeting ? 'none' : ''; + + renderDocFolderBar(); + renderDocumentsList(); + + // URL hash + window.location.hash = isMeeting ? 'documents/kokoukset' : 'documents'; +} + +document.querySelectorAll('#doc-sub-tab-bar .sub-tab').forEach(btn => { + btn.addEventListener('click', () => switchDocSubTab(btn.dataset.docSubtab)); +}); + +// ---- Dokumenttilista ---- + function renderDocumentsList() { const query = (document.getElementById('doc-search')?.value || '').toLowerCase().trim(); const filterCustomer = document.getElementById('doc-filter-customer')?.value || ''; const filterCategory = document.getElementById('doc-filter-category')?.value || ''; let filtered = allDocuments; + + // Sub-tab suodatus: kokoukset = vain kokousmuistiot + if (docSubTabMode === 'docs-kokoukset') { + filtered = filtered.filter(d => d.category === 'kokousmuistio'); + } + + // Kansiosuodatus (vain "Kaikki"-tilassa) + if (docSubTabMode !== 'docs-kokoukset') { + if (currentDocFolderId !== null) { + filtered = filtered.filter(d => d.folder_id === currentDocFolderId); + } else { + // Juuritasolla: näytä vain ilman kansiota olevat + filtered = filtered.filter(d => !d.folder_id); + } + } + if (query) { filtered = filtered.filter(d => (d.title || '').toLowerCase().includes(query) || @@ -4762,7 +4907,7 @@ function renderDocumentsList() { if (filterCustomer) { filtered = filtered.filter(d => d.customer_id === filterCustomer); } - if (filterCategory) { + if (filterCategory && docSubTabMode !== 'docs-kokoukset') { filtered = filtered.filter(d => d.category === filterCategory); } @@ -4827,14 +4972,40 @@ function renderDocReadView() { document.getElementById('doc-read-category').innerHTML = `${docCategoryLabels[d.category] || d.category || 'Muu'}`; document.getElementById('doc-read-version').textContent = `📌 Versio ${d.current_version || 0}`; document.getElementById('doc-read-date').textContent = d.muokattu ? '📅 ' + new Date(d.muokattu).toLocaleDateString('fi-FI') : ''; - document.getElementById('doc-read-description').textContent = d.description || ''; + const isMeeting = d.category === 'kokousmuistio'; + + // Kuvaus: kokousmuistioille näytetään osallistujat + if (isMeeting && d.description) { + document.getElementById('doc-read-description').textContent = 'Osallistujat: ' + d.description; + } else { + document.getElementById('doc-read-description').textContent = d.description || ''; + } // Admin-napit const isAdmin = isCurrentUserAdmin(); document.getElementById('btn-doc-delete').style.display = isAdmin ? '' : 'none'; - // Latausnappi - piilota jos ei versioita - document.getElementById('btn-doc-download').style.display = (d.current_version && d.current_version > 0) ? '' : 'none'; + // Kokousmuistio vs tiedostopohjainen + const contentSection = document.getElementById('doc-read-content-section'); + const inlineEditor = document.getElementById('doc-inline-editor'); + const fileSection = document.getElementById('doc-file-section'); + const uploadSection = document.getElementById('doc-upload-section'); + + if (isMeeting) { + // Näytä sisältö, piilota tiedosto-osiot + fileSection.style.display = 'none'; + uploadSection.style.display = 'none'; + inlineEditor.style.display = 'none'; + contentSection.style.display = ''; + const currentVersion = d.versions?.find(v => v.version_number === d.current_version); + document.getElementById('doc-read-content').textContent = currentVersion?.content || '(Tyhjä muistio)'; + } else { + // Tiedostopohjainen: piilota kokousmuistio-osiot + contentSection.style.display = 'none'; + inlineEditor.style.display = 'none'; + fileSection.style.display = (d.current_version && d.current_version > 0) ? '' : 'none'; + uploadSection.style.display = ''; + } // Versiohistoria const vtbody = document.getElementById('doc-versions-tbody'); @@ -4844,14 +5015,18 @@ function renderDocReadView() { vtbody.innerHTML = d.versions.map(v => { const date = v.luotu ? new Date(v.luotu).toLocaleDateString('fi-FI', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'; const isCurrent = v.version_number === d.current_version; + const sizeDisplay = isMeeting ? (v.content ? v.content.length + ' merkkiä' : '-') : formatFileSize(v.file_size || 0); + const downloadLink = isMeeting + ? `` + : `⬇️`; return ` v${v.version_number}${isCurrent ? ' ✓' : ''} ${date} ${esc(v.created_by || '-')} ${esc(v.change_notes || '-')} - ${formatFileSize(v.file_size || 0)} + ${sizeDisplay} - ⬇️ + ${downloadLink} ${isAdmin && !isCurrent ? `` : ''} `; @@ -4924,40 +5099,134 @@ document.getElementById('btn-doc-edit-cancel')?.addEventListener('click', () => // Uusi dokumentti document.getElementById('btn-new-document')?.addEventListener('click', () => { openDocEdit(null); }); +// Uusi kokousmuistio +document.getElementById('btn-new-meeting-note')?.addEventListener('click', () => { openDocEdit(null, 'kokousmuistio'); }); -function openDocEdit(doc) { +// Kokousmuistion inline-editori +document.getElementById('btn-doc-edit-content')?.addEventListener('click', () => { + const d = currentDocument; + if (!d) return; + const currentVersion = d.versions?.find(v => v.version_number === d.current_version); + document.getElementById('doc-inline-content').value = currentVersion?.content || ''; + document.getElementById('doc-inline-notes').value = ''; + document.getElementById('doc-inline-editor').style.display = ''; + document.getElementById('doc-read-content-section').style.display = 'none'; +}); + +document.getElementById('btn-doc-cancel-content')?.addEventListener('click', () => { + document.getElementById('doc-inline-editor').style.display = 'none'; + document.getElementById('doc-read-content-section').style.display = ''; +}); + +document.getElementById('btn-doc-save-content')?.addEventListener('click', async () => { + if (!currentDocument) return; + const content = document.getElementById('doc-inline-content').value; + const notes = document.getElementById('doc-inline-notes').value || 'Muistiota päivitetty'; + try { + currentDocument = await apiCall('document_content_save', 'POST', { + document_id: currentDocument.id, + content, + change_notes: notes + }); + renderDocReadView(); + loadDocuments(); + } catch (e) { alert('Tallennus epäonnistui: ' + e.message); } +}); + +// Katso kokousmuistion vanhaa versiota +window.viewMeetingVersion = function(versionId, versionNum) { + if (!currentDocument) return; + const v = currentDocument.versions?.find(x => x.id === versionId); + if (v) { + alert('Versio ' + versionNum + ':\n\n' + (v.content || '(Tyhjä)')); + } +}; + +// Uusi kansio +document.getElementById('btn-new-folder')?.addEventListener('click', async () => { + const name = prompt('Kansion nimi:'); + if (!name || !name.trim()) return; + try { + await apiCall('document_folder_save', 'POST', { + name: name.trim(), + parent_id: currentDocFolderId || null + }); + await loadDocuments(); + } catch (e) { alert('Kansion luonti epäonnistui: ' + e.message); } +}); + +function openDocEdit(doc, forceCategory, forceCustomerId) { document.getElementById('doc-edit-id').value = doc?.id || ''; document.getElementById('doc-edit-name').value = doc?.title || ''; document.getElementById('doc-edit-description').value = doc?.description || ''; - document.getElementById('doc-edit-category').value = doc?.category || 'muu'; - document.getElementById('doc-edit-title').textContent = doc ? 'Muokkaa dokumenttia' : 'Uusi dokumentti'; + const cat = forceCategory || doc?.category || 'muu'; + document.getElementById('doc-edit-category').value = cat; + document.getElementById('doc-edit-folder-id').value = doc?.folder_id || currentDocFolderId || ''; + + const isMeeting = cat === 'kokousmuistio'; + document.getElementById('doc-edit-title').textContent = doc + ? (isMeeting ? 'Muokkaa kokousmuistiota' : 'Muokkaa dokumenttia') + : (isMeeting ? 'Uusi kokousmuistio' : 'Uusi dokumentti'); // Täytä asiakas-dropdown const custSel = document.getElementById('doc-edit-customer'); custSel.innerHTML = ''; if (typeof customers !== 'undefined') { customers.forEach(c => { - custSel.innerHTML += ``; + custSel.innerHTML += ``; }); } - if (doc?.customer_id) custSel.value = doc.customer_id; + if (forceCustomerId) custSel.value = forceCustomerId; + else if (doc?.customer_id) custSel.value = doc.customer_id; + + // Toggle kokousmuistio vs tiedostokenttä + toggleDocMeetingFields(cat); + + // Kokousmuistio-kentät + if (isMeeting) { + const currentVersion = doc?.versions?.find(v => v.version_number === doc.current_version); + document.getElementById('doc-edit-content').value = currentVersion?.content || ''; + document.getElementById('doc-edit-participants').value = doc?.description || ''; + } else { + document.getElementById('doc-edit-content').value = ''; + document.getElementById('doc-edit-participants').value = ''; + } // Piilota tiedostokenttä muokkaustilassa (versiot hoidetaan read-viewissä) - document.getElementById('doc-edit-file').parentElement.style.display = doc ? 'none' : ''; + if (!isMeeting) { + document.getElementById('doc-edit-file').parentElement.style.display = doc ? 'none' : ''; + } showDocEditView(); } +function toggleDocMeetingFields(category) { + const isMeeting = category === 'kokousmuistio'; + document.getElementById('doc-edit-meeting-fields').style.display = isMeeting ? '' : 'none'; + document.getElementById('doc-edit-file').parentElement.style.display = isMeeting ? 'none' : ''; + document.getElementById('doc-edit-desc-group').style.display = isMeeting ? 'none' : ''; +} + +document.getElementById('doc-edit-category')?.addEventListener('change', (e) => { + toggleDocMeetingFields(e.target.value); +}); + // Lomakkeen lähetys document.getElementById('doc-edit-form')?.addEventListener('submit', async (e) => { e.preventDefault(); const id = document.getElementById('doc-edit-id').value; + const cat = document.getElementById('doc-edit-category').value; + const isMeeting = cat === 'kokousmuistio'; + const docData = { id: id || undefined, title: document.getElementById('doc-edit-name').value.trim(), - description: document.getElementById('doc-edit-description').value.trim(), - category: document.getElementById('doc-edit-category').value, + description: isMeeting + ? document.getElementById('doc-edit-participants').value.trim() + : document.getElementById('doc-edit-description').value.trim(), + category: cat, customer_id: document.getElementById('doc-edit-customer').value || null, + folder_id: document.getElementById('doc-edit-folder-id').value || null, created_by: currentUser?.username || '' }; @@ -4967,18 +5236,30 @@ document.getElementById('doc-edit-form')?.addEventListener('submit', async (e) = const saved = await apiCall('document_save', 'POST', docData); const docId = saved.id; - // Jos uusi dokumentti ja tiedosto valittu → lataa ensimmäinen versio - const fileInput = document.getElementById('doc-edit-file'); - if (!id && fileInput.files.length > 0) { - const fd = new FormData(); - fd.append('document_id', docId); - fd.append('file', fileInput.files[0]); - fd.append('change_notes', 'Ensimmäinen versio'); - const res = await fetch(`${API}?action=document_upload`, { method: 'POST', credentials: 'include', body: fd }); - const text = await res.text(); - let data; - try { data = JSON.parse(text); } catch (err) { throw new Error('Tiedoston lataus epäonnistui'); } - if (!res.ok) throw new Error(data.error || 'Virhe'); + if (isMeeting) { + // Tallenna kokousmuistion sisältö ensimmäisenä versiona + const content = document.getElementById('doc-edit-content').value; + if (content || !id) { + await apiCall('document_content_save', 'POST', { + document_id: docId, + content: content, + change_notes: id ? 'Muistiota päivitetty' : 'Ensimmäinen versio' + }); + } + } else { + // Jos uusi dokumentti ja tiedosto valittu → lataa ensimmäinen versio + const fileInput = document.getElementById('doc-edit-file'); + if (!id && fileInput.files.length > 0) { + const fd = new FormData(); + fd.append('document_id', docId); + fd.append('file', fileInput.files[0]); + fd.append('change_notes', 'Ensimmäinen versio'); + const res = await fetch(`${API}?action=document_upload`, { method: 'POST', credentials: 'include', body: fd }); + const text = await res.text(); + let data; + try { data = JSON.parse(text); } catch (err) { throw new Error('Tiedoston lataus epäonnistui'); } + if (!res.ok) throw new Error(data.error || 'Virhe'); + } } currentDocument = await apiCall(`document&id=${docId}`); diff --git a/style.css b/style.css index 4945534..9c6e576 100644 --- a/style.css +++ b/style.css @@ -1599,11 +1599,57 @@ span.empty { .doc-category.cat-ohje { background: #d1fae5; color: #065f46; } .doc-category.cat-raportti { background: #ede9fe; color: #5b21b6; } .doc-category.cat-muu { background: #f3f4f6; color: #374151; } +.doc-category.cat-kokousmuistio { background: #fce7f3; color: #9d174d; } #doc-versions-table tbody tr:hover { background: #f0f7ff !important; } +/* Dokumenttikansiot */ +.doc-folder-item { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.45rem 0.85rem; + background: #f3f4f6; + border: 1px solid #e5e7eb; + border-radius: 8px; + cursor: pointer; + font-size: 0.85rem; + font-weight: 500; + color: #374151; + transition: background 0.15s, border-color 0.15s; +} +.doc-folder-item:hover { background: #e5e7eb; } + +#doc-breadcrumbs a { color: var(--primary-color); text-decoration: none; font-weight: 500; } +#doc-breadcrumbs a:hover { text-decoration: underline; } +#doc-breadcrumbs .bc-sep { color: #aaa; margin: 0 0.25rem; } + +#doc-inline-content { + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 1rem; + font-size: 0.95rem; + resize: vertical; + width: 100%; + font-family: inherit; + line-height: 1.6; +} + +/* Asiakasprofiilin dokumentit */ +.customer-doc-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid #f0f0f0; + font-size: 0.85rem; +} +.customer-doc-item:last-child { border-bottom: none; } +.customer-doc-item a { color: var(--primary-color); text-decoration: none; font-weight: 500; } +.customer-doc-item a:hover { text-decoration: underline; } + /* ==================== LAITETILAT ==================== */ .laitetilat-grid { display: grid;