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:
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user