feat: P4 backend kvalita - SELECT * fix, overdue konsolidace, Validator

- SELECT * nahrazen explicitnimi sloupci ve 22 PHP souborech (69+ vyskytu)
- users-handlers.php: password_hash explicitne vyloucen z dotazu
- Overdue detekce presunuta do invoices.php routeru (1x pred dispatch misto 3x v handlerech)
- Validator.php: validacni helper s pravidly required, string, int, email, in, numeric
- PaginationHelper: PHPStan typy opraveny

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 18:42:42 +01:00
parent df506dfea4
commit 758be819c3
25 changed files with 513 additions and 102 deletions

View File

@@ -17,7 +17,10 @@ function handleGetAdmin(PDO $pdo): void
$endDate = date('Y-m-t', strtotime($startDate));
$sql = "
SELECT a.*, CONCAT(u.first_name, ' ', u.last_name) as user_name
SELECT a.id, a.user_id, a.shift_date, a.arrival_time, a.arrival_address,
a.break_start, a.break_end, a.departure_time, a.departure_address,
a.notes, a.project_id, a.leave_type, a.leave_hours, a.created_at,
CONCAT(u.first_name, ' ', u.last_name) as user_name
FROM attendance a
JOIN users u ON a.user_id = u.id
WHERE a.shift_date BETWEEN ? AND ?
@@ -112,7 +115,11 @@ function handleGetWorkFund(PDO $pdo): void
$startDate = sprintf('%04d-01-01', $year);
$endDate = sprintf('%04d-%02d-%02d', $year, $maxMonth, cal_days_in_month(CAL_GREGORIAN, $maxMonth, $year));
$stmt = $pdo->prepare('SELECT * FROM attendance WHERE shift_date BETWEEN ? AND ? ORDER BY shift_date');
$stmt = $pdo->prepare(
'SELECT id, user_id, shift_date, arrival_time, break_start, break_end,
departure_time, notes, project_id, leave_type, leave_hours
FROM attendance WHERE shift_date BETWEEN ? AND ? ORDER BY shift_date'
);
$stmt->execute([$startDate, $endDate]);
$allRecords = $stmt->fetchAll();
@@ -206,7 +213,13 @@ function handleGetWorkFund(PDO $pdo): void
function handleGetLocation(PDO $pdo, int $recordId): void
{
$stmt = $pdo->prepare("
SELECT a.*, CONCAT(u.first_name, ' ', u.last_name) as user_name
SELECT a.id, a.user_id, a.shift_date, a.arrival_time,
a.arrival_lat, a.arrival_lng, a.arrival_accuracy, a.arrival_address,
a.break_start, a.break_end, a.departure_time,
a.departure_lat, a.departure_lng, a.departure_accuracy,
a.departure_address, a.notes, a.project_id,
a.leave_type, a.leave_hours, a.created_at,
CONCAT(u.first_name, ' ', u.last_name) as user_name
FROM attendance a
JOIN users u ON a.user_id = u.id
WHERE a.id = ?
@@ -467,7 +480,11 @@ function handleUpdateBalance(PDO $pdo): void
function handleUpdateAttendance(PDO $pdo, int $recordId): void
{
$stmt = $pdo->prepare('SELECT * FROM attendance WHERE id = ?');
$stmt = $pdo->prepare(
'SELECT id, user_id, shift_date, arrival_time, break_start, break_end,
departure_time, notes, project_id, leave_type, leave_hours
FROM attendance WHERE id = ?'
);
$stmt->execute([$recordId]);
$record = $stmt->fetch();
@@ -593,7 +610,10 @@ function handleUpdateAttendance(PDO $pdo, int $recordId): void
function handleDeleteAttendance(PDO $pdo, int $recordId): void
{
$stmt = $pdo->prepare('SELECT * FROM attendance WHERE id = ?');
$stmt = $pdo->prepare(
'SELECT id, user_id, shift_date, leave_type, leave_hours
FROM attendance WHERE id = ?'
);
$stmt->execute([$recordId]);
$record = $stmt->fetch();
@@ -920,7 +940,10 @@ function handleGetPrint(PDO $pdo): void
$users = $stmt->fetchAll();
$sql = "
SELECT a.*, CONCAT(u.first_name, ' ', u.last_name) as user_name
SELECT a.id, a.user_id, a.shift_date, a.arrival_time, a.arrival_address,
a.break_start, a.break_end, a.departure_time, a.departure_address,
a.notes, a.project_id, a.leave_type, a.leave_hours, a.created_at,
CONCAT(u.first_name, ' ', u.last_name) as user_name
FROM attendance a
JOIN users u ON a.user_id = u.id
WHERE a.shift_date BETWEEN ? AND ?

View File

@@ -217,7 +217,9 @@ function enrichRecordsWithProjectLogs(PDO $pdo, array &$records): void
if (!empty($recordIds)) {
$placeholders = implode(',', array_fill(0, count($recordIds), '?'));
$stmt = $pdo->prepare(
"SELECT * FROM attendance_project_logs WHERE attendance_id IN ($placeholders) ORDER BY started_at ASC"
"SELECT id, attendance_id, project_id, started_at, ended_at, hours, minutes
FROM attendance_project_logs
WHERE attendance_id IN ($placeholders) ORDER BY started_at ASC"
);
$stmt->execute($recordIds);
foreach ($stmt->fetchAll() as $log) {

View File

@@ -460,7 +460,9 @@ class AuditLog
// Get logs
$sql = "
SELECT *
SELECT id, user_id, username, user_ip, action,
entity_type, entity_id, description,
old_values, new_values, created_at
FROM audit_logs
$whereClause
ORDER BY created_at DESC
@@ -503,7 +505,9 @@ class AuditLog
$pdo = db();
$stmt = $pdo->prepare('
SELECT *
SELECT id, user_id, username, user_ip, action,
entity_type, entity_id, description,
old_values, new_values, created_at
FROM audit_logs
WHERE user_id = ?
ORDER BY created_at DESC
@@ -531,7 +535,9 @@ class AuditLog
$pdo = db();
$stmt = $pdo->prepare('
SELECT *
SELECT id, user_id, username, user_ip, action,
entity_type, entity_id, description,
old_values, new_values, created_at
FROM audit_logs
WHERE entity_type = ? AND entity_id = ?
ORDER BY created_at DESC

View File

@@ -245,8 +245,11 @@ class JWTAuth
// First check if token exists (regardless of expiry)
$stmt = $pdo->prepare('
SELECT rt.*, u.id as user_id, u.username, u.email, u.first_name, u.last_name,
u.is_active, r.name as role_name, r.display_name as role_display_name
SELECT rt.id, rt.user_id, rt.token_hash, rt.expires_at,
rt.replaced_at, rt.remember_me,
u.id as user_id, u.username, u.email,
u.first_name, u.last_name, u.is_active,
r.name as role_name, r.display_name as role_display_name
FROM refresh_tokens rt
JOIN users u ON rt.user_id = u.id
LEFT JOIN roles r ON u.role_id = r.id

View File

@@ -14,6 +14,7 @@ class PaginationHelper
/**
* Nacte pagination parametry z GET requestu.
*
* @param array<string, string> $sortMap
* @return array{page: int, per_page: int, sort: string, order: string, search: string}
*/
public static function parseParams(array $sortMap, string $defaultSort = 'created_at'): array
@@ -43,9 +44,10 @@ class PaginationHelper
* @param PDO $pdo
* @param string $countSql - COUNT(*) dotaz
* @param string $dataSql - SELECT dotaz (bez LIMIT/OFFSET)
* @param array $params - parametry pro prepared statement
* @param array<int, mixed> $params - parametry pro prepared statement
* @param array{page: int, per_page: int, sort: string, order: string} $pagination
* @return array{items: array, pagination: array}
* @return array{items: array<int, array<string, mixed>>,
* pagination: array{total: int, page: int, per_page: int, total_pages: int}}
*/
public static function paginate(
PDO $pdo,

139
api/includes/Validator.php Normal file
View File

@@ -0,0 +1,139 @@
<?php
/**
* Validacni helper pro API vstupy.
*
* Pouziti:
* $v = new Validator($input);
* $v->required('name')->string('name', 1, 255);
* $v->required('email')->email('email');
* $v->int('amount', 0, 1000000);
* $v->in('status', ['active', 'inactive']);
* if ($v->fails()) errorResponse($v->firstError());
*/
declare(strict_types=1);
class Validator
{
/** @var array<string, mixed> */
private array $data;
/** @var array<string, string> */
private array $errors = [];
/** @param array<string, mixed> $data */
public function __construct(array $data)
{
$this->data = $data;
}
public function required(string $field, string $label = ''): self
{
$value = $this->data[$field] ?? null;
if ($value === null || $value === '') {
$this->errors[$field] = ($label ?: $field) . ' je povinné pole';
}
return $this;
}
public function string(string $field, int $min = 0, int $max = 0, string $label = ''): self
{
$value = $this->data[$field] ?? null;
if ($value === null || $value === '') {
return $this;
}
if (!is_string($value)) {
$this->errors[$field] = ($label ?: $field) . ' musí být text';
return $this;
}
$len = mb_strlen($value);
if ($min > 0 && $len < $min) {
$this->errors[$field] = ($label ?: $field) . " musí mít alespoň {$min} znaků";
} elseif ($max > 0 && $len > $max) {
$this->errors[$field] = ($label ?: $field) . " nesmí překročit {$max} znaků";
}
return $this;
}
public function int(string $field, ?int $min = null, ?int $max = null, string $label = ''): self
{
$value = $this->data[$field] ?? null;
if ($value === null || $value === '') {
return $this;
}
if (!is_numeric($value)) {
$this->errors[$field] = ($label ?: $field) . ' musí být číslo';
return $this;
}
$intVal = (int) $value;
if ($min !== null && $intVal < $min) {
$this->errors[$field] = ($label ?: $field) . " musí být alespoň {$min}";
} elseif ($max !== null && $intVal > $max) {
$this->errors[$field] = ($label ?: $field) . " nesmí překročit {$max}";
}
return $this;
}
public function email(string $field, string $label = ''): self
{
$value = $this->data[$field] ?? null;
if ($value === null || $value === '') {
return $this;
}
if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$this->errors[$field] = ($label ?: $field) . ' musí být platný e-mail';
}
return $this;
}
/**
* @param list<string> $allowed
*/
public function in(string $field, array $allowed, string $label = ''): self
{
$value = $this->data[$field] ?? null;
if ($value === null || $value === '') {
return $this;
}
if (!in_array($value, $allowed, true)) {
$this->errors[$field] = ($label ?: $field) . ' má neplatnou hodnotu';
}
return $this;
}
public function numeric(string $field, ?float $min = null, ?float $max = null, string $label = ''): self
{
$value = $this->data[$field] ?? null;
if ($value === null || $value === '') {
return $this;
}
if (!is_numeric($value)) {
$this->errors[$field] = ($label ?: $field) . ' musí být číslo';
return $this;
}
$numVal = (float) $value;
if ($min !== null && $numVal < $min) {
$this->errors[$field] = ($label ?: $field) . " musí být alespoň {$min}";
} elseif ($max !== null && $numVal > $max) {
$this->errors[$field] = ($label ?: $field) . " nesmí překročit {$max}";
}
return $this;
}
public function fails(): bool
{
return count($this->errors) > 0;
}
public function firstError(): string
{
return reset($this->errors) ?: '';
}
/** @return array<string, string> */
public function errors(): array
{
return $this->errors;
}
}