Versioiva dokumentinhallinta + Laitetilat-moduuli

Dokumentit: versioiva tiedostonhallinta asiakkaille (sopimukset, laskut, ohjeet).
Sisältää versiohistorian, tiedostojen latauksen/palautuksen ja asiakas-suodatuksen.

Laitetilat: laitetilojen hallinta kuvagallerialla ja tiedostolistauksella.
Sisältää korttipohjaisen listanäkymän, kuvien esikatselun ja tiedostojen hallinnan.

Molemmat moduulit: 4 DB-taulua, 14 API-endpointtia, täysi CRUD, tiedostoupload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 15:18:32 +02:00
parent 093f40ac09
commit e6fa65165e
5 changed files with 1612 additions and 2 deletions

253
db.php
View File

@@ -502,6 +502,65 @@ function initDatabase(): void {
FOREIGN KEY (todo_id) REFERENCES todos(id) ON DELETE CASCADE,
INDEX idx_todo (todo_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
"CREATE TABLE IF NOT EXISTS documents (
id VARCHAR(20) PRIMARY KEY,
company_id VARCHAR(50) NOT NULL,
customer_id VARCHAR(20) DEFAULT NULL,
title VARCHAR(255) NOT NULL,
description TEXT DEFAULT '',
category VARCHAR(50) DEFAULT 'muu',
current_version INT DEFAULT 0,
created_by VARCHAR(100) DEFAULT '',
luotu DATETIME,
muokattu DATETIME,
muokkaaja VARCHAR(100) DEFAULT '',
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
INDEX idx_company (company_id),
INDEX idx_customer (customer_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
"CREATE TABLE IF NOT EXISTS document_versions (
id VARCHAR(20) PRIMARY KEY,
document_id VARCHAR(20) NOT NULL,
version_number INT NOT NULL,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_size INT DEFAULT 0,
mime_type VARCHAR(100) DEFAULT '',
change_notes TEXT DEFAULT '',
created_by VARCHAR(100) DEFAULT '',
luotu DATETIME,
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE,
INDEX idx_document (document_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
"CREATE TABLE IF NOT EXISTS laitetilat (
id VARCHAR(20) PRIMARY KEY,
company_id VARCHAR(50) NOT NULL,
nimi VARCHAR(255) NOT NULL,
kuvaus TEXT DEFAULT '',
osoite VARCHAR(255) DEFAULT '',
luotu DATETIME,
muokattu DATETIME,
muokkaaja VARCHAR(100) DEFAULT '',
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
INDEX idx_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
"CREATE TABLE IF NOT EXISTS laitetila_files (
id VARCHAR(20) PRIMARY KEY,
laitetila_id VARCHAR(20) NOT NULL,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_size INT DEFAULT 0,
mime_type VARCHAR(100) DEFAULT '',
description VARCHAR(500) DEFAULT '',
created_by VARCHAR(100) DEFAULT '',
luotu DATETIME,
FOREIGN KEY (laitetila_id) REFERENCES laitetilat(id) ON DELETE CASCADE,
INDEX idx_laitetila (laitetila_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
];
foreach ($tables as $i => $sql) {
@@ -1625,3 +1684,197 @@ function dbToggleTodoSubtask(string $subtaskId): bool {
function dbDeleteTodoSubtask(string $subtaskId): void {
_dbExecute("DELETE FROM todo_subtasks WHERE id = ?", [$subtaskId]);
}
// ==================== DOKUMENTIT ====================
function dbLoadDocuments(string $companyId, ?string $customerId = null): array {
$sql = "SELECT d.*,
dv.original_name AS current_file, dv.file_size AS current_size, dv.created_by AS version_author, dv.luotu AS version_date
FROM documents d
LEFT JOIN document_versions dv ON dv.document_id = d.id AND dv.version_number = d.current_version
WHERE d.company_id = :companyId";
$params = ['companyId' => $companyId];
if ($customerId !== null) {
$sql .= " AND d.customer_id = :customerId";
$params['customerId'] = $customerId;
}
$sql .= " ORDER BY d.muokattu DESC, d.luotu DESC";
return _dbFetchAll($sql, $params);
}
function dbLoadDocument(string $documentId): ?array {
$doc = _dbFetchOne("SELECT * FROM documents WHERE id = ?", [$documentId]);
if (!$doc) return null;
$doc['versions'] = _dbFetchAll(
"SELECT * FROM document_versions WHERE document_id = ? ORDER BY version_number DESC",
[$documentId]
);
return $doc;
}
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)
ON DUPLICATE KEY UPDATE
title = VALUES(title),
description = VALUES(description),
category = VALUES(category),
customer_id = VALUES(customer_id),
muokattu = VALUES(muokattu),
muokkaaja = VALUES(muokkaaja)
", [
'id' => $id,
'companyId' => $companyId,
'customerId' => !empty($doc['customer_id']) ? $doc['customer_id'] : null,
'title' => $doc['title'] ?? '',
'description' => $doc['description'] ?? '',
'category' => $doc['category'] ?? 'muu',
'currentVersion' => (int)($doc['current_version'] ?? 0),
'createdBy' => $doc['created_by'] ?? '',
'luotu' => $doc['luotu'] ?? $now,
'muokattu' => $now,
'muokkaaja' => $doc['muokkaaja'] ?? ''
]);
return $id;
}
function dbDeleteDocument(string $documentId): ?array {
// Palauta dokumentin tiedot tiedostojen poistoa varten
$doc = _dbFetchOne("SELECT id, company_id FROM documents WHERE id = ?", [$documentId]);
if ($doc) {
_dbExecute("DELETE FROM documents WHERE id = ?", [$documentId]); // CASCADE poistaa versiot
}
return $doc;
}
function dbAddDocumentVersion(string $documentId, array $version): void {
$now = date('Y-m-d H:i:s');
// Hae seuraava versionumero
$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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
$version['id'] ?? generateId(),
$documentId,
$nextVersion,
$version['filename'] ?? '',
$version['original_name'] ?? '',
$version['file_size'] ?? 0,
$version['mime_type'] ?? '',
$version['change_notes'] ?? '',
$version['created_by'] ?? '',
$now
]);
// Päivitä dokumentin current_version
_dbExecute("UPDATE documents SET current_version = ?, muokattu = ?, muokkaaja = ? WHERE id = ?", [
$nextVersion, $now, $version['created_by'] ?? '', $documentId
]);
}
function dbRestoreDocumentVersion(string $documentId, string $versionId, string $user): ?int {
// Hae palautettava versio
$oldVersion = _dbFetchOne("SELECT * FROM document_versions WHERE id = ? AND document_id = ?", [$versionId, $documentId]);
if (!$oldVersion) return null;
// Lisää uutena versiona (kopioi tiedostotiedot)
$now = date('Y-m-d H:i:s');
$maxVersion = _dbFetchScalar("SELECT COALESCE(MAX(version_number), 0) FROM document_versions WHERE document_id = ?", [$documentId]);
$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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
$newId,
$documentId,
$nextVersion,
$oldVersion['filename'],
$oldVersion['original_name'],
$oldVersion['file_size'],
$oldVersion['mime_type'],
'Palautettu versiosta ' . $oldVersion['version_number'],
$user,
$now
]);
_dbExecute("UPDATE documents SET current_version = ?, muokattu = ?, muokkaaja = ? WHERE id = ?", [
$nextVersion, $now, $user, $documentId
]);
return $nextVersion;
}
function dbGetDocumentVersion(string $documentId, int $versionNumber): ?array {
return _dbFetchOne("SELECT * FROM document_versions WHERE document_id = ? AND version_number = ?", [$documentId, $versionNumber]);
}
// ==================== LAITETILAT ====================
function dbLoadLaitetilat(string $companyId): array {
$tilat = _dbFetchAll("SELECT * FROM laitetilat WHERE company_id = ? ORDER BY nimi", [$companyId]);
foreach ($tilat as &$t) {
$t['file_count'] = (int)_dbFetchScalar("SELECT COUNT(*) FROM laitetila_files WHERE laitetila_id = ?", [$t['id']]);
}
return $tilat;
}
function dbLoadLaitetila(string $laitetilaId): ?array {
$tila = _dbFetchOne("SELECT * FROM laitetilat WHERE id = ?", [$laitetilaId]);
if (!$tila) return null;
$tila['files'] = _dbFetchAll("SELECT * FROM laitetila_files WHERE laitetila_id = ? ORDER BY luotu DESC", [$laitetilaId]);
return $tila;
}
function dbSaveLaitetila(string $companyId, array $tila): string {
$id = $tila['id'] ?? generateId();
$now = date('Y-m-d H:i:s');
_dbExecute("
INSERT INTO laitetilat (id, company_id, nimi, kuvaus, osoite, luotu, muokattu, muokkaaja)
VALUES (:id, :companyId, :nimi, :kuvaus, :osoite, :luotu, :muokattu, :muokkaaja)
ON DUPLICATE KEY UPDATE
nimi = VALUES(nimi),
kuvaus = VALUES(kuvaus),
osoite = VALUES(osoite),
muokattu = VALUES(muokattu),
muokkaaja = VALUES(muokkaaja)
", [
'id' => $id,
'companyId' => $companyId,
'nimi' => $tila['nimi'] ?? '',
'kuvaus' => $tila['kuvaus'] ?? '',
'osoite' => $tila['osoite'] ?? '',
'luotu' => $tila['luotu'] ?? $now,
'muokattu' => $now,
'muokkaaja' => $tila['muokkaaja'] ?? ''
]);
return $id;
}
function dbDeleteLaitetila(string $laitetilaId): ?array {
$tila = _dbFetchOne("SELECT id, company_id FROM laitetilat WHERE id = ?", [$laitetilaId]);
if ($tila) {
_dbExecute("DELETE FROM laitetilat WHERE id = ?", [$laitetilaId]); // CASCADE poistaa tiedostot
}
return $tila;
}
function dbAddLaitetilaFile(string $laitetilaId, array $file): void {
_dbExecute("INSERT INTO laitetila_files (id, laitetila_id, filename, original_name, file_size, mime_type, description, created_by, luotu) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
$file['id'] ?? generateId(),
$laitetilaId,
$file['filename'] ?? '',
$file['original_name'] ?? '',
$file['file_size'] ?? 0,
$file['mime_type'] ?? '',
$file['description'] ?? '',
$file['created_by'] ?? '',
date('Y-m-d H:i:s')
]);
}
function dbDeleteLaitetilaFile(string $fileId): ?array {
$file = _dbFetchOne("SELECT lf.*, lt.company_id FROM laitetila_files lf JOIN laitetilat lt ON lt.id = lf.laitetila_id WHERE lf.id = ?", [$fileId]);
if ($file) {
_dbExecute("DELETE FROM laitetila_files WHERE id = ?", [$fileId]);
}
return $file;
}