- 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>
424 lines
13 KiB
PHP
424 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
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');
|
|
}
|