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

447
api.php
View File

@@ -3994,6 +3994,453 @@ switch ($action) {
echo json_encode(['success' => true]);
break;
// ==================== DOKUMENTIT ====================
case 'documents':
requireAuth();
$companyId = requireCompany();
try {
$customerId = $_GET['customer_id'] ?? null;
$docs = dbLoadDocuments($companyId, $customerId ?: null);
echo json_encode($docs);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Dokumenttien haku epäonnistui: ' . $e->getMessage()]);
}
break;
case 'document':
requireAuth();
$companyId = requireCompany();
try {
$docId = $_GET['id'] ?? '';
if (empty($docId)) {
http_response_code(400);
echo json_encode(['error' => 'Dokumentin ID puuttuu']);
break;
}
$doc = dbLoadDocument($docId);
if (!$doc || $doc['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
break;
}
echo json_encode($doc);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Dokumentin haku epäonnistui: ' . $e->getMessage()]);
}
break;
case 'document_save':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
try {
$input = json_decode(file_get_contents('php://input'), true);
$input['created_by'] = $input['created_by'] ?? currentUser();
$input['muokkaaja'] = currentUser();
$id = dbSaveDocument($companyId, $input);
$doc = dbLoadDocument($id);
echo json_encode($doc);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Dokumentin tallennus epäonnistui: ' . $e->getMessage()]);
}
break;
case 'document_upload':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
try {
$docId = $_POST['document_id'] ?? '';
if (empty($docId)) {
http_response_code(400);
echo json_encode(['error' => 'Dokumentin ID puuttuu']);
break;
}
// Tarkista dokumentin olemassaolo
$doc = dbLoadDocument($docId);
if (!$doc || $doc['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
break;
}
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['error' => 'Tiedosto puuttuu tai lähetys epäonnistui']);
break;
}
$file = $_FILES['file'];
// Max 50MB
if ($file['size'] > 50 * 1024 * 1024) {
http_response_code(400);
echo json_encode(['error' => 'Tiedosto on liian suuri (max 50MB)']);
break;
}
// Luo hakemisto
$docDir = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId;
if (!file_exists($docDir)) mkdir($docDir, 0755, true);
$nextVersion = (int)$doc['current_version'] + 1;
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$safeFilename = $nextVersion . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $file['name']);
move_uploaded_file($file['tmp_name'], $docDir . '/' . $safeFilename);
$mimeType = '';
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $docDir . '/' . $safeFilename);
finfo_close($finfo);
}
dbAddDocumentVersion($docId, [
'filename' => $safeFilename,
'original_name' => $file['name'],
'file_size' => $file['size'],
'mime_type' => $mimeType ?: ($file['type'] ?? ''),
'change_notes' => $_POST['change_notes'] ?? '',
'created_by' => currentUser()
]);
$updatedDoc = dbLoadDocument($docId);
echo json_encode($updatedDoc);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Tiedoston lataus epäonnistui: ' . $e->getMessage()]);
}
break;
case 'document_download':
requireAuth();
$companyId = requireCompany();
try {
$docId = $_GET['id'] ?? '';
$versionNum = (int)($_GET['version'] ?? 0);
if (empty($docId)) {
http_response_code(400);
echo json_encode(['error' => 'Dokumentin ID puuttuu']);
break;
}
$doc = dbLoadDocument($docId);
if (!$doc || $doc['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
break;
}
// Jos versiota ei annettu, käytä nykyistä
if ($versionNum <= 0) $versionNum = (int)$doc['current_version'];
$version = dbGetDocumentVersion($docId, $versionNum);
if (!$version) {
http_response_code(404);
echo json_encode(['error' => 'Versiota ei löytynyt']);
break;
}
$filePath = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId . '/' . $version['filename'];
if (!file_exists($filePath)) {
http_response_code(404);
echo json_encode(['error' => 'Tiedostoa ei löytynyt palvelimelta']);
break;
}
header('Content-Type: ' . ($version['mime_type'] ?: 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . $version['original_name'] . '"');
header('Content-Length: ' . filesize($filePath));
readfile($filePath);
exit;
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Lataus epäonnistui: ' . $e->getMessage()]);
}
break;
case 'document_restore':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
try {
$input = json_decode(file_get_contents('php://input'), true);
$docId = $input['document_id'] ?? '';
$versionId = $input['version_id'] ?? '';
if (empty($docId) || empty($versionId)) {
http_response_code(400);
echo json_encode(['error' => 'Parametrit puuttuvat']);
break;
}
$doc = dbLoadDocument($docId);
if (!$doc || $doc['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
break;
}
// Hae vanha versio ja kopioi tiedosto
$oldVersion = null;
foreach ($doc['versions'] as $v) {
if ($v['id'] === $versionId) { $oldVersion = $v; break; }
}
if (!$oldVersion) {
http_response_code(404);
echo json_encode(['error' => 'Versiota ei löytynyt']);
break;
}
// Kopioi tiedosto uudella nimellä
$docDir = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId;
$oldFilePath = $docDir . '/' . $oldVersion['filename'];
if (file_exists($oldFilePath)) {
$nextVersion = (int)$doc['current_version'] + 1;
$newFilename = $nextVersion . '_' . $oldVersion['original_name'];
$newFilename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $newFilename);
copy($oldFilePath, $docDir . '/' . $newFilename);
}
$result = dbRestoreDocumentVersion($docId, $versionId, currentUser());
if ($result === null) {
http_response_code(400);
echo json_encode(['error' => 'Palautus epäonnistui']);
break;
}
$updatedDoc = dbLoadDocument($docId);
echo json_encode($updatedDoc);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Palautus epäonnistui: ' . $e->getMessage()]);
}
break;
case 'document_delete':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
try {
$input = json_decode(file_get_contents('php://input'), true);
$docId = $input['id'] ?? '';
$doc = dbLoadDocument($docId);
if (!$doc || $doc['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Dokumenttia ei löytynyt']);
break;
}
// Poista tiedostot levyltä
$docDir = DATA_DIR . '/companies/' . $companyId . '/documents/' . $docId;
if (is_dir($docDir)) {
$files = glob($docDir . '/*');
foreach ($files as $f) { if (is_file($f)) unlink($f); }
rmdir($docDir);
}
dbDeleteDocument($docId);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Poisto epäonnistui: ' . $e->getMessage()]);
}
break;
// ==================== LAITETILAT ====================
case 'laitetilat':
requireAuth();
$companyId = requireCompany();
try {
$tilat = dbLoadLaitetilat($companyId);
echo json_encode($tilat);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Laitetilojen haku epäonnistui: ' . $e->getMessage()]);
}
break;
case 'laitetila':
requireAuth();
$companyId = requireCompany();
try {
$tilaId = $_GET['id'] ?? '';
if (empty($tilaId)) {
http_response_code(400);
echo json_encode(['error' => 'Laitetilan ID puuttuu']);
break;
}
$tila = dbLoadLaitetila($tilaId);
if (!$tila || $tila['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Laitetilaa ei löytynyt']);
break;
}
echo json_encode($tila);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Laitetilan haku epäonnistui: ' . $e->getMessage()]);
}
break;
case 'laitetila_save':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
try {
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input['nimi'])) {
http_response_code(400);
echo json_encode(['error' => 'Laitetilan nimi puuttuu']);
break;
}
$input['muokkaaja'] = currentUser();
$id = dbSaveLaitetila($companyId, $input);
$tila = dbLoadLaitetila($id);
echo json_encode($tila);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Laitetilan tallennus epäonnistui: ' . $e->getMessage()]);
}
break;
case 'laitetila_delete':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
try {
$input = json_decode(file_get_contents('php://input'), true);
$tilaId = $input['id'] ?? '';
$tila = dbLoadLaitetila($tilaId);
if (!$tila || $tila['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Laitetilaa ei löytynyt']);
break;
}
// Poista tiedostot levyltä
$tilaDir = DATA_DIR . '/companies/' . $companyId . '/laitetilat/' . $tilaId;
if (is_dir($tilaDir)) {
$files = glob($tilaDir . '/*');
foreach ($files as $f) { if (is_file($f)) unlink($f); }
rmdir($tilaDir);
}
dbDeleteLaitetila($tilaId);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Poisto epäonnistui: ' . $e->getMessage()]);
}
break;
case 'laitetila_file_upload':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
try {
$tilaId = $_POST['laitetila_id'] ?? '';
if (empty($tilaId)) {
http_response_code(400);
echo json_encode(['error' => 'Laitetilan ID puuttuu']);
break;
}
$tila = dbLoadLaitetila($tilaId);
if (!$tila || $tila['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Laitetilaa ei löytynyt']);
break;
}
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['error' => 'Tiedosto puuttuu tai lähetys epäonnistui']);
break;
}
$file = $_FILES['file'];
if ($file['size'] > 50 * 1024 * 1024) {
http_response_code(400);
echo json_encode(['error' => 'Tiedosto on liian suuri (max 50MB)']);
break;
}
$tilaDir = DATA_DIR . '/companies/' . $companyId . '/laitetilat/' . $tilaId;
if (!file_exists($tilaDir)) mkdir($tilaDir, 0755, true);
$safeFilename = time() . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $file['name']);
move_uploaded_file($file['tmp_name'], $tilaDir . '/' . $safeFilename);
$mimeType = '';
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $tilaDir . '/' . $safeFilename);
finfo_close($finfo);
}
dbAddLaitetilaFile($tilaId, [
'filename' => $safeFilename,
'original_name' => $file['name'],
'file_size' => $file['size'],
'mime_type' => $mimeType ?: ($file['type'] ?? ''),
'description' => $_POST['description'] ?? '',
'created_by' => currentUser()
]);
$updatedTila = dbLoadLaitetila($tilaId);
echo json_encode($updatedTila);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Tiedoston lataus epäonnistui: ' . $e->getMessage()]);
}
break;
case 'laitetila_file_delete':
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
try {
$input = json_decode(file_get_contents('php://input'), true);
$fileId = $input['id'] ?? '';
$file = dbDeleteLaitetilaFile($fileId);
if ($file) {
$filePath = DATA_DIR . '/companies/' . $file['company_id'] . '/laitetilat/' . $file['laitetila_id'] . '/' . $file['filename'];
if (file_exists($filePath)) unlink($filePath);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Tiedoston poisto epäonnistui: ' . $e->getMessage()]);
}
break;
case 'laitetila_file_download':
requireAuth();
$companyId = requireCompany();
try {
$tilaId = $_GET['laitetila_id'] ?? '';
$fileId = $_GET['file_id'] ?? '';
if (empty($tilaId) || empty($fileId)) {
http_response_code(400);
echo json_encode(['error' => 'Parametrit puuttuvat']);
break;
}
$tila = dbLoadLaitetila($tilaId);
if (!$tila || $tila['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Laitetilaa ei löytynyt']);
break;
}
$targetFile = null;
foreach ($tila['files'] as $f) {
if ($f['id'] === $fileId) { $targetFile = $f; break; }
}
if (!$targetFile) {
http_response_code(404);
echo json_encode(['error' => 'Tiedostoa ei löytynyt']);
break;
}
$filePath = DATA_DIR . '/companies/' . $companyId . '/laitetilat/' . $tilaId . '/' . $targetFile['filename'];
if (!file_exists($filePath)) {
http_response_code(404);
echo json_encode(['error' => 'Tiedostoa ei löytynyt palvelimelta']);
break;
}
header('Content-Type: ' . ($targetFile['mime_type'] ?: 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . $targetFile['original_name'] . '"');
header('Content-Length: ' . filesize($filePath));
readfile($filePath);
exit;
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Lataus epäonnistui: ' . $e->getMessage()]);
}
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Tuntematon toiminto']);