refactor: odstraneni PSR-1 SideEffects warningu

- Handler funkce extrahovany z API souboru do api/admin/handlers/
- config.php rozdeleny na helpers.php (funkce) a constants.php (konstanty)
- require_once odstranen z class souboru (AuditLog, JWTAuth, LeaveNotification)
- vendor/autoload.php presunuto do config.php bootstrap
- totp-handlers.php: pridany use deklarace pro TwoFactorAuth
- phpstan.neon: bootstrapFiles, scanDirectories, dynamicConstantNames
- Opraveny chybejici routing bloky v totp.php a session.php

Vysledek: phpcs 0 errors 0 warnings, PHPStan 0 errors, ESLint 0 errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 14:29:21 +01:00
parent f733dee856
commit 5ef6fc8064
45 changed files with 7105 additions and 6924 deletions

View File

@@ -18,6 +18,7 @@ declare(strict_types=1);
require_once dirname(__DIR__) . '/config.php';
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
require_once dirname(__DIR__) . '/includes/AuditLog.php';
require_once __DIR__ . '/handlers/projects-handlers.php';
setCorsHeaders();
setSecurityHeaders();
@@ -111,423 +112,3 @@ try {
}
// --- Number generation ---
function generateProjectNumber(PDO $pdo): string
{
return generateSharedNumber($pdo);
}
function handleGetNextNumber(PDO $pdo): void
{
$number = generateProjectNumber($pdo);
successResponse(['number' => $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<string, mixed> $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<string, mixed> $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');
}