Dokumentit: kokousmuistiot, kansiorakenne, sub-tabit, asiakaslinkkaus
- 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 <noreply@anthropic.com>
This commit is contained in:
84
api.php
84
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':
|
||||
|
||||
63
db.php
63
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
|
||||
|
||||
57
index.html
57
index.html
@@ -730,13 +730,26 @@
|
||||
|
||||
<!-- Tab: Dokumentit -->
|
||||
<div class="tab-content" id="tab-content-documents">
|
||||
<div class="sub-tab-bar" id="doc-sub-tab-bar">
|
||||
<button class="sub-tab active" data-doc-subtab="docs-all">Kaikki</button>
|
||||
<button class="sub-tab" data-doc-subtab="docs-kokoukset">Kokoukset</button>
|
||||
</div>
|
||||
<div class="main-container">
|
||||
<!-- Listanäkymä -->
|
||||
<div id="docs-list-view">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem;">
|
||||
<h3 style="color:var(--primary-dark);margin:0;">📄 Dokumentit</h3>
|
||||
<h3 style="color:var(--primary-dark);margin:0;" id="docs-list-title">📄 Dokumentit</h3>
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<button class="btn-primary" id="btn-new-document">+ Uusi dokumentti</button>
|
||||
<button class="btn-primary" id="btn-new-meeting-note" style="display:none;">+ Uusi kokousmuistio</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Kansionavigointi -->
|
||||
<div id="doc-folder-bar" style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;flex-wrap:wrap;">
|
||||
<span id="doc-breadcrumbs" style="font-size:0.9rem;color:#555;"></span>
|
||||
<button class="btn-secondary" id="btn-new-folder" style="font-size:0.8rem;padding:0.25rem 0.6rem;">+ Kansio</button>
|
||||
</div>
|
||||
<div id="doc-folders-grid" style="display:flex;gap:0.5rem;margin-bottom:0.75rem;flex-wrap:wrap;"></div>
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap;">
|
||||
<input type="text" id="doc-search" placeholder="Hae dokumentteja..." style="flex:1;min-width:150px;">
|
||||
<select id="doc-filter-customer" style="min-width:140px;">
|
||||
@@ -748,6 +761,7 @@
|
||||
<option value="lasku">Lasku</option>
|
||||
<option value="ohje">Ohje</option>
|
||||
<option value="raportti">Raportti</option>
|
||||
<option value="kokousmuistio">Kokousmuistio</option>
|
||||
<option value="muu">Muu</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -792,13 +806,31 @@
|
||||
</div>
|
||||
<p id="doc-read-description" style="color:#555;margin-bottom:1.5rem;"></p>
|
||||
|
||||
<!-- Nykyisen version lataus -->
|
||||
<div style="margin-bottom:1.5rem;">
|
||||
<!-- Kokousmuistion sisältö -->
|
||||
<div id="doc-read-content-section" style="display:none;margin-bottom:1.5rem;">
|
||||
<div id="doc-read-content" style="white-space:pre-wrap;background:#fefefe;border:1px solid #e5e7eb;padding:1rem;border-radius:8px;min-height:100px;line-height:1.6;font-size:0.95rem;"></div>
|
||||
<div style="margin-top:0.75rem;">
|
||||
<button class="btn-primary" id="btn-doc-edit-content">✏️ Muokkaa muistiota</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kokousmuistion inline-editori -->
|
||||
<div id="doc-inline-editor" style="display:none;margin-bottom:1.5rem;">
|
||||
<textarea id="doc-inline-content" rows="15" placeholder="Kirjoita kokousmuistio tähän..."></textarea>
|
||||
<div style="margin-top:0.5rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
|
||||
<input type="text" id="doc-inline-notes" placeholder="Muistiinpanot versiosta (valinnainen)" style="flex:1;min-width:200px;">
|
||||
<button class="btn-primary" id="btn-doc-save-content">Tallenna</button>
|
||||
<button class="btn-secondary" id="btn-doc-cancel-content">Peruuta</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nykyisen version lataus (tiedostopohjaisille) -->
|
||||
<div id="doc-file-section" style="margin-bottom:1.5rem;">
|
||||
<button class="btn-primary" id="btn-doc-download" style="font-size:1rem;padding:0.7rem 1.5rem;">⬇️ Lataa tiedosto</button>
|
||||
</div>
|
||||
|
||||
<!-- Uusi versio -->
|
||||
<div style="background:#f8f9fb;padding:1rem;border-radius:10px;margin-bottom:1.5rem;">
|
||||
<!-- Uusi versio (tiedostopohjaisille) -->
|
||||
<div id="doc-upload-section" style="background:#f8f9fb;padding:1rem;border-radius:10px;margin-bottom:1.5rem;">
|
||||
<h4 style="margin:0 0 0.75rem;color:var(--primary-dark);">Lataa uusi versio</h4>
|
||||
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:end;">
|
||||
<input type="file" id="doc-version-file" style="flex:1;min-width:200px;">
|
||||
@@ -848,6 +880,7 @@
|
||||
<option value="lasku">Lasku</option>
|
||||
<option value="ohje">Ohje</option>
|
||||
<option value="raportti">Raportti</option>
|
||||
<option value="kokousmuistio">Kokousmuistio</option>
|
||||
<option value="muu">Muu</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -861,11 +894,23 @@
|
||||
<label>Tiedosto</label>
|
||||
<input type="file" id="doc-edit-file">
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<div class="form-group full-width" id="doc-edit-desc-group">
|
||||
<label>Kuvaus</label>
|
||||
<textarea id="doc-edit-description" rows="3" placeholder="Dokumentin kuvaus..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Kokousmuistion kentät -->
|
||||
<div id="doc-edit-meeting-fields" style="display:none;margin-top:1rem;">
|
||||
<div class="form-group" style="margin-bottom:0.75rem;">
|
||||
<label>Osallistujat</label>
|
||||
<input type="text" id="doc-edit-participants" placeholder="Nimi1, Nimi2, Nimi3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Muistion sisältö</label>
|
||||
<textarea id="doc-edit-content" rows="15" placeholder="Kirjoita kokousmuistio tähän..." style="font-family:inherit;line-height:1.6;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="doc-edit-folder-id">
|
||||
<div style="margin-top:1rem;display:flex;gap:0.5rem;">
|
||||
<button type="submit" class="btn-primary">Tallenna</button>
|
||||
<button type="button" class="btn-secondary" id="btn-doc-edit-cancel">Peruuta</button>
|
||||
|
||||
307
script.js
307
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) {
|
||||
<span 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>
|
||||
<div class="detail-section"><h3>Dokumentit</h3>
|
||||
<div id="customer-docs-list" style="margin-bottom:0.75rem;"><span style="color:#aaa;font-size:0.85rem;">Ladataan...</span></div>
|
||||
<button class="btn-primary" style="font-size:0.85rem;padding:6px 14px;" onclick="openDocEditForCustomer('${id}')">+ Lisää dokumentti</button>
|
||||
<button class="btn-primary" style="font-size:0.85rem;padding:6px 14px;margin-left:0.25rem;" onclick="openDocEditForCustomer('${id}', 'kokousmuistio')">+ Kokousmuistio</button>
|
||||
</div>`;
|
||||
|
||||
// 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 = '<p style="color:#aaa;font-size:0.85rem;">Ei dokumentteja.</p>';
|
||||
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 `<div class="customer-doc-item">
|
||||
<a href="#" onclick="openDocFromCustomer('${d.id}');return false;">${esc(d.title)}</a>
|
||||
<span><span class="doc-category cat-${d.category || 'muu'}" style="font-size:0.75rem;">${catLabel}</span> <span style="color:#aaa;font-size:0.8rem;">${date}</span></span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = '<p style="color:red;font-size:0.85rem;">Virhe ladattaessa dokumentteja.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
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 = `<a href="#" onclick="navigateDocFolder(null);return false;">📁 Kaikki</a>`;
|
||||
if (currentDocFolderId) {
|
||||
const path = getDocFolderPath(currentDocFolderId);
|
||||
path.forEach(f => {
|
||||
crumbs += `<span class="bc-sep">/</span><a href="#" onclick="navigateDocFolder('${f.id}');return false;">${esc(f.name)}</a>`;
|
||||
});
|
||||
}
|
||||
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 =>
|
||||
`<div class="doc-folder-item" onclick="navigateDocFolder('${f.id}')">📁 ${esc(f.name)}</div>`
|
||||
).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 = `<span class="doc-category cat-${d.category || 'muu'}">${docCategoryLabels[d.category] || d.category || 'Muu'}</span>`;
|
||||
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') : '';
|
||||
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
|
||||
? `<button onclick="viewMeetingVersion('${v.id}', ${v.version_number})" title="Näytä versio" style="background:none;border:none;cursor:pointer;font-size:1rem;">👁️</button>`
|
||||
: `<a href="${API}?action=document_download&id=${d.id}&version=${v.version_number}" target="_blank" title="Lataa">⬇️</a>`;
|
||||
return `<tr${isCurrent ? ' style="background:#f0f7ff;"' : ''}>
|
||||
<td style="font-weight:600;">v${v.version_number}${isCurrent ? ' ✓' : ''}</td>
|
||||
<td>${date}</td>
|
||||
<td>${esc(v.created_by || '-')}</td>
|
||||
<td>${esc(v.change_notes || '-')}</td>
|
||||
<td>${formatFileSize(v.file_size || 0)}</td>
|
||||
<td>${sizeDisplay}</td>
|
||||
<td class="actions-cell">
|
||||
<a href="${API}?action=document_download&id=${d.id}&version=${v.version_number}" target="_blank" title="Lataa">⬇️</a>
|
||||
${downloadLink}
|
||||
${isAdmin && !isCurrent ? `<button onclick="restoreDocVersion('${d.id}', '${v.id}', ${v.version_number})" title="Palauta tämä versio" style="background:none;border:none;cursor:pointer;font-size:1rem;">🔄</button>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
@@ -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 = '<option value="">Ei asiakasta (yleinen)</option>';
|
||||
if (typeof customers !== 'undefined') {
|
||||
customers.forEach(c => {
|
||||
custSel.innerHTML += `<option value="${c.id}" ${doc?.customer_id === c.id ? 'selected' : ''}>${esc(c.yritys)}</option>`;
|
||||
custSel.innerHTML += `<option value="${c.id}" ${(forceCustomerId === c.id || doc?.customer_id === c.id) ? 'selected' : ''}>${esc(c.yritys)}</option>`;
|
||||
});
|
||||
}
|
||||
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ä)
|
||||
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,6 +5236,17 @@ document.getElementById('doc-edit-form')?.addEventListener('submit', async (e) =
|
||||
const saved = await apiCall('document_save', 'POST', docData);
|
||||
const docId = saved.id;
|
||||
|
||||
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) {
|
||||
@@ -4980,6 +5260,7 @@ document.getElementById('doc-edit-form')?.addEventListener('submit', async (e) =
|
||||
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}`);
|
||||
renderDocReadView();
|
||||
|
||||
46
style.css
46
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;
|
||||
|
||||
Reference in New Issue
Block a user