$number]); } function handleCreateProject(PDO $pdo): void { $input = getJsonInput(); $name = trim($input['name'] ?? ''); if (!$name) { errorResponse('Název projektu je povinný'); } if (mb_strlen($name) > 255) { errorResponse('Název projektu je příliš dlouhý (max 255 znaků)'); } $customerId = isset($input['customer_id']) ? (int)$input['customer_id'] : null; if (!$customerId) { errorResponse('Zákazník je povinný'); } // Verify customer exists $stmt = $pdo->prepare('SELECT id FROM customers WHERE id = ?'); $stmt->execute([$customerId]); if (!$stmt->fetch()) { errorResponse('Zákazník nebyl nalezen', 404); } $startDate = $input['start_date'] ?? date('Y-m-d'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) { errorResponse('Neplatný formát data zahájení'); } $projectNumber = trim($input['project_number'] ?? ''); if ($projectNumber && mb_strlen($projectNumber) > 50) { errorResponse('Číslo projektu je příliš dlouhé (max 50 znaků)'); } // Lock for concurrent number generation $locked = $pdo->query("SELECT GET_LOCK('boha_project_number', 5)")->fetchColumn(); if (!$locked) { errorResponse('Nepodařilo se získat zámek pro číslo projektu, zkuste to znovu', 503); } $pdo->beginTransaction(); try { // Generate or validate number if (!$projectNumber) { $projectNumber = generateProjectNumber($pdo); } else { // Validate uniqueness against both tables $stmt = $pdo->prepare('SELECT id FROM orders WHERE order_number = ?'); $stmt->execute([$projectNumber]); if ($stmt->fetch()) { $pdo->rollBack(); $pdo->query("SELECT RELEASE_LOCK('boha_project_number')"); errorResponse('Číslo projektu je již použito jako číslo objednávky'); } $stmt = $pdo->prepare('SELECT id FROM projects WHERE project_number = ?'); $stmt->execute([$projectNumber]); if ($stmt->fetch()) { $pdo->rollBack(); $pdo->query("SELECT RELEASE_LOCK('boha_project_number')"); errorResponse('Číslo projektu je již použito'); } } $stmt = $pdo->prepare(" INSERT INTO projects ( project_number, name, customer_id, status, start_date, created_at, modified_at ) VALUES (?, ?, ?, 'aktivni', ?, NOW(), NOW()) "); $stmt->execute([ $projectNumber, $name, $customerId, $startDate, ]); $projectId = (int)$pdo->lastInsertId(); $pdo->commit(); $pdo->query("SELECT RELEASE_LOCK('boha_project_number')"); AuditLog::logCreate('projects_project', $projectId, [ 'project_number' => $projectNumber, 'name' => $name, 'customer_id' => $customerId, ], "Ručně vytvořen projekt '$projectNumber'"); successResponse([ 'project_id' => $projectId, 'project_number' => $projectNumber, ], 'Projekt byl vytvořen'); } catch (PDOException $e) { $pdo->rollBack(); $pdo->query("SELECT RELEASE_LOCK('boha_project_number')"); throw $e; } } function handleDeleteProject(PDO $pdo, int $id): void { $stmt = $pdo->prepare('SELECT * FROM projects WHERE id = ?'); $stmt->execute([$id]); $project = $stmt->fetch(); if (!$project) { errorResponse('Projekt nebyl nalezen', 404); } // Only manually created projects (without order_id) can be deleted if (!empty($project['order_id'])) { errorResponse('Projekt propojený s objednávkou nelze smazat. Smažte objednávku.', 400); } $pdo->beginTransaction(); try { // Delete project notes $stmt = $pdo->prepare('DELETE FROM project_notes WHERE project_id = ?'); $stmt->execute([$id]); // Delete project $stmt = $pdo->prepare('DELETE FROM projects WHERE id = ?'); $stmt->execute([$id]); $pdo->commit(); AuditLog::logUpdate( 'projects_project', $id, ['status' => $project['status']], ['status' => 'deleted'], "Smazán ruční projekt '{$project['project_number']}'" ); successResponse(null, 'Projekt byl smazán'); } catch (PDOException $e) { $pdo->rollBack(); throw $e; } } function handleGetList(PDO $pdo): void { $search = trim($_GET['search'] ?? ''); $sort = $_GET['sort'] ?? 'created_at'; $order = strtoupper($_GET['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC'; $page = max(1, (int) ($_GET['page'] ?? 1)); $perPage = min(500, max(1, (int) ($_GET['per_page'] ?? 500))); $sortMap = [ 'ProjectNumber' => 'p.project_number', 'project_number' => 'p.project_number', 'Name' => 'p.name', 'name' => 'p.name', 'Status' => 'p.status', 'status' => 'p.status', 'StartDate' => 'p.start_date', 'start_date' => 'p.start_date', 'EndDate' => 'p.end_date', 'end_date' => 'p.end_date', 'CreatedAt' => 'p.created_at', 'created_at' => 'p.created_at', ]; if (!isset($sortMap[$sort])) { errorResponse('Neplatný parametr řazení', 400); } $sortCol = $sortMap[$sort]; $where = 'WHERE 1=1'; $params = []; if ($search) { $search = mb_substr($search, 0, 100); $where .= ' AND (p.project_number LIKE ? OR p.name LIKE ? OR c.name LIKE ?)'; $searchParam = "%{$search}%"; $params = [$searchParam, $searchParam, $searchParam]; } $countSql = " SELECT COUNT(*) FROM projects p LEFT JOIN customers c ON p.customer_id = c.id LEFT JOIN orders o ON p.order_id = o.id $where "; $stmt = $pdo->prepare($countSql); $stmt->execute($params); $total = (int) $stmt->fetchColumn(); $offset = ($page - 1) * $perPage; $sql = " SELECT p.id, p.project_number, p.name, p.status, p.start_date, p.end_date, p.order_id, p.quotation_id, p.created_at, c.name as customer_name, o.order_number FROM projects p LEFT JOIN customers c ON p.customer_id = c.id LEFT JOIN orders o ON p.order_id = o.id $where ORDER BY $sortCol $order LIMIT $perPage OFFSET $offset "; $stmt = $pdo->prepare($sql); $stmt->execute($params); $projects = $stmt->fetchAll(); successResponse([ 'projects' => $projects, 'total' => $total, 'page' => $page, 'per_page' => $perPage, ]); } function handleGetDetail(PDO $pdo, int $id): void { $stmt = $pdo->prepare(' SELECT p.*, c.name as customer_name, o.order_number, o.status as order_status, q.quotation_number FROM projects p LEFT JOIN customers c ON p.customer_id = c.id LEFT JOIN orders o ON p.order_id = o.id LEFT JOIN quotations q ON p.quotation_id = q.id WHERE p.id = ? '); $stmt->execute([$id]); $project = $stmt->fetch(); if (!$project) { errorResponse('Projekt nebyl nalezen', 404); } successResponse($project); } function handleUpdateProject(PDO $pdo, int $id): void { $stmt = $pdo->prepare('SELECT * FROM projects WHERE id = ?'); $stmt->execute([$id]); $project = $stmt->fetch(); if (!$project) { errorResponse('Projekt nebyl nalezen', 404); } $input = getJsonInput(); // Validace statusu if (isset($input['status'])) { $validStatuses = ['aktivni', 'dokonceny', 'zruseny']; if (!in_array($input['status'], $validStatuses)) { errorResponse('Neplatný stav projektu'); } } // Validace dat if ( isset($input['start_date']) && $input['start_date'] !== null // @phpstan-ignore notIdentical.alwaysTrue && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $input['start_date']) ) { errorResponse('Neplatný formát data zahájení'); } if ( isset($input['end_date']) && $input['end_date'] !== null // @phpstan-ignore notIdentical.alwaysTrue && $input['end_date'] !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $input['end_date']) ) { errorResponse('Neplatný formát data ukončení'); } // Delkove limity $name = $input['name'] ?? $project['name']; if (mb_strlen($name) > 255) { errorResponse('Název projektu je příliš dlouhý (max 255 znaků)'); } $notes = $input['notes'] ?? $project['notes']; if ($notes !== null && mb_strlen($notes) > 5000) { errorResponse('Poznámky jsou příliš dlouhé (max 5000 znaků)'); } $pdo->beginTransaction(); try { $stmt = $pdo->prepare(' UPDATE projects SET name = ?, status = ?, start_date = ?, end_date = ?, notes = ?, modified_at = NOW() WHERE id = ? '); $stmt->execute([ $name, $input['status'] ?? $project['status'], $input['start_date'] ?? $project['start_date'], $input['end_date'] ?? $project['end_date'], $notes, $id, ]); $pdo->commit(); AuditLog::logUpdate( 'projects_project', $id, ['name' => $project['name'], 'status' => $project['status']], ['name' => $input['name'] ?? $project['name'], 'status' => $input['status'] ?? $project['status']], "Upraven projekt '{$project['project_number']}'" ); successResponse(null, 'Projekt byl aktualizován'); } catch (PDOException $e) { $pdo->rollBack(); throw $e; } } function handleGetNotes(PDO $pdo, int $projectId): void { // Verify project exists $stmt = $pdo->prepare('SELECT id FROM projects WHERE id = ?'); $stmt->execute([$projectId]); if (!$stmt->fetch()) { errorResponse('Projekt nebyl nalezen', 404); } $stmt = $pdo->prepare(' SELECT id, project_id, user_id, user_name, content, created_at FROM project_notes WHERE project_id = ? ORDER BY created_at DESC '); $stmt->execute([$projectId]); $notes = $stmt->fetchAll(); successResponse(['notes' => $notes]); } /** @param array $authData */ function handleAddNote(PDO $pdo, int $projectId, array $authData): void { // Verify project exists $stmt = $pdo->prepare('SELECT id FROM projects WHERE id = ?'); $stmt->execute([$projectId]); if (!$stmt->fetch()) { errorResponse('Projekt nebyl nalezen', 404); } $input = getJsonInput(); $content = trim($input['content'] ?? ''); if (!$content) { errorResponse('Text poznámky je povinný'); } if (mb_strlen($content) > 5000) { errorResponse('Poznámka je příliš dlouhá (max 5000 znaků)'); } $userName = $authData['user']['full_name'] ?? $authData['user']['username'] ?? 'Neznámý'; $stmt = $pdo->prepare(' INSERT INTO project_notes (project_id, user_id, user_name, content, created_at) VALUES (?, ?, ?, ?, NOW()) '); $stmt->execute([$projectId, $authData['user_id'], $userName, $content]); $noteId = (int)$pdo->lastInsertId(); // Fetch the new note $stmt = $pdo->prepare( 'SELECT id, project_id, user_id, user_name, content, created_at FROM project_notes WHERE id = ?' ); $stmt->execute([$noteId]); $note = $stmt->fetch(); successResponse(['note' => $note], 'Poznámka byla přidána'); } /** @param array $authData */ function handleDeleteNote(PDO $pdo, int $noteId, array $authData): void { // Only admins can delete notes $isAdmin = $authData['user']['is_admin'] ?? false; if (!$isAdmin) { errorResponse('Pouze administrátoři mohou mazat poznámky', 403); } $stmt = $pdo->prepare('SELECT id, project_id, content FROM project_notes WHERE id = ?'); $stmt->execute([$noteId]); $note = $stmt->fetch(); if (!$note) { errorResponse('Poznámka nebyla nalezena', 404); } $stmt = $pdo->prepare('DELETE FROM project_notes WHERE id = ?'); $stmt->execute([$noteId]); successResponse(null, 'Poznámka byla smazána'); }