Dokumenttien poisto kaikille + versioiden säilytysrajoitus

- Poisto-nappi näkyy dokumentin luojalle (ei enää vain admin)
- API: document_delete sallii poiston adminille tai luojalle
- Uusi max_versions-sarake documents-tauluun (oletus 10)
- Versioiden automaattinen pruning: uuden version tallennuksen yhteydessä
  poistetaan vanhimmat versiot jos yli max_versions (tiedostot levyltä myös)
- Valittavissa per dokumentti: 5, 10, 20, 50 tai rajaton

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 23:35:50 +02:00
parent f40b387383
commit cb52dbfabe
4 changed files with 57 additions and 6 deletions

View File

@@ -4283,7 +4283,7 @@ switch ($action) {
break; break;
case 'document_delete': case 'document_delete':
requireAdmin(); requireAuth();
$companyId = requireCompany(); $companyId = requireCompany();
if ($method !== 'POST') break; if ($method !== 'POST') break;
try { try {
@@ -4295,6 +4295,12 @@ switch ($action) {
echo json_encode(['error' => 'Dokumenttia ei löytynyt']); echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
break; break;
} }
// Salli poisto adminille tai dokumentin luojalle
if (!isCompanyAdmin() && $doc['created_by'] !== currentUser()) {
http_response_code(403);
echo json_encode(['error' => 'Ei oikeutta poistaa']);
break;
}
// Poista tiedostot levyltä // Poista tiedostot levyltä
$docDir = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId; $docDir = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId;
if (is_dir($docDir)) { if (is_dir($docDir)) {

35
db.php
View File

@@ -612,6 +612,7 @@ function initDatabase(): void {
"ALTER TABLE todos ADD COLUMN category VARCHAR(30) DEFAULT '' AFTER priority", "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 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 documents ADD COLUMN folder_id VARCHAR(20) DEFAULT NULL AFTER customer_id",
"ALTER TABLE documents ADD COLUMN max_versions INT DEFAULT 10 AFTER current_version",
"ALTER TABLE document_versions ADD COLUMN content MEDIUMTEXT DEFAULT NULL AFTER mime_type", "ALTER TABLE document_versions ADD COLUMN content MEDIUMTEXT DEFAULT NULL AFTER mime_type",
]; ];
foreach ($alters as $sql) { foreach ($alters as $sql) {
@@ -1818,14 +1819,15 @@ function dbSaveDocument(string $companyId, array $doc): string {
$id = $doc['id'] ?? generateId(); $id = $doc['id'] ?? generateId();
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
_dbExecute(" _dbExecute("
INSERT INTO documents (id, company_id, customer_id, folder_id, title, description, category, current_version, created_by, luotu, muokattu, muokkaaja) INSERT INTO documents (id, company_id, customer_id, folder_id, title, description, category, current_version, max_versions, created_by, luotu, muokattu, muokkaaja)
VALUES (:id, :companyId, :customerId, :folderId, :title, :description, :category, :currentVersion, :createdBy, :luotu, :muokattu, :muokkaaja) VALUES (:id, :companyId, :customerId, :folderId, :title, :description, :category, :currentVersion, :maxVersions, :createdBy, :luotu, :muokattu, :muokkaaja)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
title = VALUES(title), title = VALUES(title),
description = VALUES(description), description = VALUES(description),
category = VALUES(category), category = VALUES(category),
customer_id = VALUES(customer_id), customer_id = VALUES(customer_id),
folder_id = VALUES(folder_id), folder_id = VALUES(folder_id),
max_versions = VALUES(max_versions),
muokattu = VALUES(muokattu), muokattu = VALUES(muokattu),
muokkaaja = VALUES(muokkaaja) muokkaaja = VALUES(muokkaaja)
", [ ", [
@@ -1837,6 +1839,7 @@ function dbSaveDocument(string $companyId, array $doc): string {
'description' => $doc['description'] ?? '', 'description' => $doc['description'] ?? '',
'category' => $doc['category'] ?? 'muu', 'category' => $doc['category'] ?? 'muu',
'currentVersion' => (int)($doc['current_version'] ?? 0), 'currentVersion' => (int)($doc['current_version'] ?? 0),
'maxVersions' => (int)($doc['max_versions'] ?? 10),
'createdBy' => $doc['created_by'] ?? '', 'createdBy' => $doc['created_by'] ?? '',
'luotu' => $doc['luotu'] ?? $now, 'luotu' => $doc['luotu'] ?? $now,
'muokattu' => $now, 'muokattu' => $now,
@@ -1877,6 +1880,34 @@ function dbAddDocumentVersion(string $documentId, array $version): void {
_dbExecute("UPDATE documents SET current_version = ?, muokattu = ?, muokkaaja = ? WHERE id = ?", [ _dbExecute("UPDATE documents SET current_version = ?, muokattu = ?, muokkaaja = ? WHERE id = ?", [
$nextVersion, $now, $version['created_by'] ?? '', $documentId $nextVersion, $now, $version['created_by'] ?? '', $documentId
]); ]);
// Versioiden pruning: poista vanhimmat jos yli max_versions
_pruneDocumentVersions($documentId);
}
function _pruneDocumentVersions(string $documentId): void {
$doc = _dbFetchOne("SELECT max_versions, company_id FROM documents WHERE id = ?", [$documentId]);
if (!$doc) return;
$maxVersions = (int)($doc['max_versions'] ?? 10);
if ($maxVersions <= 0) return; // 0 = rajaton
$versions = _dbFetchAll(
"SELECT id, version_number, filename FROM document_versions WHERE document_id = ? ORDER BY version_number DESC",
[$documentId]
);
if (count($versions) <= $maxVersions) return;
// Poista vanhimmat versiot (säilytä uusimmat $maxVersions kpl)
$toDelete = array_slice($versions, $maxVersions);
foreach ($toDelete as $v) {
// Poista tiedosto levyltä jos olemassa
if (!empty($v['filename'])) {
$filePath = DATA_DIR . '/companies/' . $doc['company_id'] . '/documents/' . $documentId . '/' . $v['filename'];
if (is_file($filePath)) unlink($filePath);
}
_dbExecute("DELETE FROM document_versions WHERE id = ?", [$v['id']]);
}
} }
function dbRestoreDocumentVersion(string $documentId, string $versionId, string $user): ?int { function dbRestoreDocumentVersion(string $documentId, string $versionId, string $user): ?int {

View File

@@ -894,6 +894,16 @@
<label>Tiedosto</label> <label>Tiedosto</label>
<input type="file" id="doc-edit-file"> <input type="file" id="doc-edit-file">
</div> </div>
<div class="form-group">
<label>Versioita säilytetään</label>
<select id="doc-edit-max-versions">
<option value="5">5 versiota</option>
<option value="10" selected>10 versiota</option>
<option value="20">20 versiota</option>
<option value="50">50 versiota</option>
<option value="0">Rajaton</option>
</select>
</div>
<div class="form-group full-width" id="doc-edit-desc-group"> <div class="form-group full-width" id="doc-edit-desc-group">
<label>Kuvaus</label> <label>Kuvaus</label>
<textarea id="doc-edit-description" rows="3" placeholder="Dokumentin kuvaus..."></textarea> <textarea id="doc-edit-description" rows="3" placeholder="Dokumentin kuvaus..."></textarea>

View File

@@ -4970,7 +4970,8 @@ function renderDocReadView() {
document.getElementById('doc-read-title').textContent = d.title || ''; document.getElementById('doc-read-title').textContent = d.title || '';
document.getElementById('doc-read-customer').textContent = '👤 ' + customerName; document.getElementById('doc-read-customer').textContent = '👤 ' + customerName;
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-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}`; const maxV = (d.max_versions && d.max_versions > 0) ? d.max_versions : '∞';
document.getElementById('doc-read-version').textContent = `📌 Versio ${d.current_version || 0} (max ${maxV})`;
document.getElementById('doc-read-date').textContent = d.muokattu ? '📅 ' + new Date(d.muokattu).toLocaleDateString('fi-FI') : ''; document.getElementById('doc-read-date').textContent = d.muokattu ? '📅 ' + new Date(d.muokattu).toLocaleDateString('fi-FI') : '';
const isMeeting = d.category === 'kokousmuistio'; const isMeeting = d.category === 'kokousmuistio';
@@ -4981,9 +4982,10 @@ function renderDocReadView() {
document.getElementById('doc-read-description').textContent = d.description || ''; document.getElementById('doc-read-description').textContent = d.description || '';
} }
// Admin-napit // Poista-nappi: näytetään adminille tai dokumentin luojalle
const isAdmin = isCurrentUserAdmin(); const isAdmin = isCurrentUserAdmin();
document.getElementById('btn-doc-delete').style.display = isAdmin ? '' : 'none'; const isOwner = d.created_by === (currentUser?.username || '');
document.getElementById('btn-doc-delete').style.display = (isAdmin || isOwner) ? '' : 'none';
// Kokousmuistio vs tiedostopohjainen // Kokousmuistio vs tiedostopohjainen
const contentSection = document.getElementById('doc-read-content-section'); const contentSection = document.getElementById('doc-read-content-section');
@@ -5162,6 +5164,7 @@ function openDocEdit(doc, forceCategory, forceCustomerId) {
const cat = forceCategory || doc?.category || 'muu'; const cat = forceCategory || doc?.category || 'muu';
document.getElementById('doc-edit-category').value = cat; document.getElementById('doc-edit-category').value = cat;
document.getElementById('doc-edit-folder-id').value = doc?.folder_id || currentDocFolderId || ''; document.getElementById('doc-edit-folder-id').value = doc?.folder_id || currentDocFolderId || '';
document.getElementById('doc-edit-max-versions').value = doc?.max_versions ?? 10;
const isMeeting = cat === 'kokousmuistio'; const isMeeting = cat === 'kokousmuistio';
document.getElementById('doc-edit-title').textContent = doc document.getElementById('doc-edit-title').textContent = doc
@@ -5227,6 +5230,7 @@ document.getElementById('doc-edit-form')?.addEventListener('submit', async (e) =
category: cat, category: cat,
customer_id: document.getElementById('doc-edit-customer').value || null, customer_id: document.getElementById('doc-edit-customer').value || null,
folder_id: document.getElementById('doc-edit-folder-id').value || null, folder_id: document.getElementById('doc-edit-folder-id').value || null,
max_versions: parseInt(document.getElementById('doc-edit-max-versions').value) || 10,
created_by: currentUser?.username || '' created_by: currentUser?.username || ''
}; };