prepare(' SELECT COALESCE( (SELECT MAX(end_km) FROM trips WHERE vehicle_id = ?), (SELECT initial_km FROM vehicles WHERE id = ?), 0 ) as last_km '); $stmt->execute([$vehicleId, $vehicleId]); $result = $stmt->fetch(); return $result ? (int)$result['last_km'] : 0; } function formatKm(int $km): string { return number_format($km, 0, ',', ' ') . ' km'; } // ============================================================================ // GET Handlers // ============================================================================ /** * GET - Current month trips (filtered to current user) */ function handleGetCurrent(PDO $pdo, int $userId): void { $month = validateMonth(); $vehicleId = isset($_GET['vehicle_id']) ? (int)$_GET['vehicle_id'] : null; $startDate = "{$month}-01"; $endDate = date('Y-m-t', strtotime($startDate)); $sql = " SELECT t.id, t.vehicle_id, t.user_id, t.trip_date, t.start_km, t.end_km, t.distance, t.route_from, t.route_to, t.is_business, t.notes, t.created_at, v.spz, v.name as vehicle_name, v.brand, v.model, CONCAT(u.first_name, ' ', u.last_name) as driver_name FROM trips t JOIN vehicles v ON t.vehicle_id = v.id JOIN users u ON t.user_id = u.id WHERE t.trip_date BETWEEN ? AND ? AND t.user_id = ? "; $params = [$startDate, $endDate, $userId]; if ($vehicleId) { $sql .= ' AND t.vehicle_id = ?'; $params[] = $vehicleId; } $sql .= ' ORDER BY t.trip_date DESC, t.start_km DESC'; $stmt = $pdo->prepare($sql); $stmt->execute($params); $trips = $stmt->fetchAll(); // Get active vehicles for selection $stmt = $pdo->query('SELECT id, spz, name, brand, model FROM vehicles WHERE is_active = 1 ORDER BY name'); $vehicles = $stmt->fetchAll(); // Calculate totals $totalDistance = 0; $businessDistance = 0; $privateDistance = 0; foreach ($trips as $trip) { $totalDistance += $trip['distance']; if ($trip['is_business']) { $businessDistance += $trip['distance']; } else { $privateDistance += $trip['distance']; } } successResponse([ 'trips' => $trips, 'vehicles' => $vehicles, 'month' => $month, 'totals' => [ 'total' => $totalDistance, 'business' => $businessDistance, 'private' => $privateDistance, 'count' => count($trips), ], ]); } /** * GET - Trip history with filters (filtered to current user) */ function handleGetHistory(PDO $pdo, int $userId): void { $month = validateMonth(); $vehicleId = isset($_GET['vehicle_id']) ? (int)$_GET['vehicle_id'] : null; $startDate = "{$month}-01"; $endDate = date('Y-m-t', strtotime($startDate)); $sql = " SELECT t.id, t.vehicle_id, t.user_id, t.trip_date, t.start_km, t.end_km, t.distance, t.route_from, t.route_to, t.is_business, t.notes, t.created_at, v.spz, v.name as vehicle_name, v.brand, v.model, CONCAT(u.first_name, ' ', u.last_name) as driver_name FROM trips t JOIN vehicles v ON t.vehicle_id = v.id JOIN users u ON t.user_id = u.id WHERE t.trip_date BETWEEN ? AND ? AND t.user_id = ? "; $params = [$startDate, $endDate, $userId]; if ($vehicleId) { $sql .= ' AND t.vehicle_id = ?'; $params[] = $vehicleId; } $sql .= ' ORDER BY t.trip_date DESC, t.start_km DESC'; $stmt = $pdo->prepare($sql); $stmt->execute($params); $trips = $stmt->fetchAll(); // Get vehicles for filter $stmt = $pdo->query('SELECT id, spz, name FROM vehicles WHERE is_active = 1 ORDER BY name'); $vehicles = $stmt->fetchAll(); // Calculate totals $totalDistance = 0; $businessDistance = 0; foreach ($trips as $trip) { $totalDistance += $trip['distance']; if ($trip['is_business']) { $businessDistance += $trip['distance']; } } successResponse([ 'trips' => $trips, 'vehicles' => $vehicles, 'month' => $month, 'totals' => [ 'total' => $totalDistance, 'business' => $businessDistance, 'count' => count($trips), ], ]); } /** * GET - Admin view of all trips */ function handleGetAdmin(PDO $pdo): void { $dateFrom = $_GET['date_from'] ?? null; $dateTo = $_GET['date_to'] ?? null; $vehicleId = isset($_GET['vehicle_id']) ? (int)$_GET['vehicle_id'] : null; $filterUserId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null; // Default to current month if no dates provided if (!$dateFrom || !$dateTo) { $month = date('Y-m'); $startDate = "{$month}-01"; $endDate = date('Y-m-t', strtotime($startDate)); } else { if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) { errorResponse('Neplatný formát data (očekáváno YYYY-MM-DD)'); } $startDate = $dateFrom; $endDate = $dateTo; } $sql = " SELECT t.id, t.vehicle_id, t.user_id, t.trip_date, t.start_km, t.end_km, t.distance, t.route_from, t.route_to, t.is_business, t.notes, t.created_at, v.spz, v.name as vehicle_name, CONCAT(u.first_name, ' ', u.last_name) as driver_name FROM trips t JOIN vehicles v ON t.vehicle_id = v.id JOIN users u ON t.user_id = u.id WHERE t.trip_date BETWEEN ? AND ? "; $params = [$startDate, $endDate]; if ($vehicleId) { $sql .= ' AND t.vehicle_id = ?'; $params[] = $vehicleId; } if ($filterUserId) { $sql .= ' AND t.user_id = ?'; $params[] = $filterUserId; } $sql .= ' ORDER BY t.trip_date DESC, t.start_km DESC'; $stmt = $pdo->prepare($sql); $stmt->execute($params); $trips = $stmt->fetchAll(); // Get vehicles for filter $stmt = $pdo->query('SELECT id, spz, name FROM vehicles ORDER BY name'); $vehicles = $stmt->fetchAll(); // Get users for filter $stmt = $pdo->query( "SELECT id, CONCAT(first_name, ' ', last_name) as name FROM users WHERE is_active = 1 ORDER BY last_name" ); $users = $stmt->fetchAll(); // Calculate totals $totalDistance = 0; $businessDistance = 0; foreach ($trips as $trip) { $totalDistance += $trip['distance']; if ($trip['is_business']) { $businessDistance += $trip['distance']; } } successResponse([ 'trips' => $trips, 'vehicles' => $vehicles, 'users' => $users, 'date_from' => $startDate, 'date_to' => $endDate, 'totals' => [ 'total' => $totalDistance, 'business' => $businessDistance, 'count' => count($trips), ], ]); } /** * GET - All vehicles (admin) */ function handleGetVehicles(PDO $pdo): void { $stmt = $pdo->query(' SELECT v.id, v.spz, v.name, v.brand, v.model, v.initial_km, v.actual_km, v.is_active, v.created_at, v.updated_at, COUNT(t.id) as trip_count, COALESCE(MAX(t.end_km), v.initial_km) as current_km FROM vehicles v LEFT JOIN trips t ON t.vehicle_id = v.id GROUP BY v.id ORDER BY v.is_active DESC, v.name '); $vehicles = $stmt->fetchAll(); successResponse(['vehicles' => $vehicles]); } /** * GET - Active vehicles for selection */ function handleGetActiveVehicles(PDO $pdo): void { $stmt = $pdo->query(' SELECT v.id, v.spz, v.name, v.brand, v.model, COALESCE(MAX(t.end_km), v.initial_km) as current_km FROM vehicles v LEFT JOIN trips t ON t.vehicle_id = v.id WHERE v.is_active = 1 GROUP BY v.id ORDER BY v.name '); $vehicles = $stmt->fetchAll(); successResponse(['vehicles' => $vehicles]); } /** * GET - Last km for vehicle */ function handleGetLastKm(PDO $pdo): void { $vehicleId = (int)($_GET['vehicle_id'] ?? 0); if (!$vehicleId) { errorResponse('Vehicle ID je povinné'); } $lastKm = getLastKmForVehicle($pdo, $vehicleId); successResponse(['last_km' => $lastKm]); } // ============================================================================ // POST Handlers // ============================================================================ /** * POST - Create trip */ function handleCreateTrip(PDO $pdo, int $userId): void { $input = getJsonInput(); $vehicleId = (int)($input['vehicle_id'] ?? 0); $tripDate = $input['trip_date'] ?? ''; $startKm = (int)($input['start_km'] ?? 0); $endKm = (int)($input['end_km'] ?? 0); $routeFrom = trim($input['route_from'] ?? ''); $routeTo = trim($input['route_to'] ?? ''); $isBusiness = (int)($input['is_business'] ?? 1); $notes = trim($input['notes'] ?? ''); // Validation if (!$vehicleId) { errorResponse('Vyberte vozidlo'); } if (!$tripDate) { errorResponse('Datum jízdy je povinné'); } if (!$startKm) { errorResponse('Počáteční stav km je povinný'); } if (!$endKm) { errorResponse('Konečný stav km je povinný'); } if (!$routeFrom) { errorResponse('Místo odjezdu je povinné'); } if (!$routeTo) { errorResponse('Místo příjezdu je povinné'); } if ($endKm <= $startKm) { errorResponse('Konečný stav km musí být větší než počáteční'); } // Check vehicle exists $stmt = $pdo->prepare('SELECT id FROM vehicles WHERE id = ? AND is_active = 1'); $stmt->execute([$vehicleId]); if (!$stmt->fetch()) { errorResponse('Vozidlo neexistuje nebo není aktivní'); } $stmt = $pdo->prepare(' INSERT INTO trips (vehicle_id, user_id, trip_date, start_km, end_km, route_from, route_to, is_business, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) '); $stmt->execute([ $vehicleId, $userId, $tripDate, $startKm, $endKm, $routeFrom, $routeTo, $isBusiness, $notes ?: null, ]); $newId = (int)$pdo->lastInsertId(); AuditLog::logCreate('trips', $newId, $input, 'Vytvořen záznam jízdy'); successResponse(['id' => $newId], 'Jízda byla zaznamenána'); } /** * POST - Create/update vehicle (admin) */ function handleVehicle(PDO $pdo): void { $input = getJsonInput(); $id = (int)($input['id'] ?? 0); $spz = strtoupper(trim($input['spz'] ?? '')); $name = trim($input['name'] ?? ''); $brand = trim($input['brand'] ?? ''); $model = trim($input['model'] ?? ''); $initialKm = (int)($input['initial_km'] ?? 0); $isActive = isset($input['is_active']) ? (int)$input['is_active'] : 1; if (!$spz) { errorResponse('SPZ je povinná'); } if (!$name) { errorResponse('Název je povinný'); } if ($id) { // Update $stmt = $pdo->prepare(' UPDATE vehicles SET spz = ?, name = ?, brand = ?, model = ?, initial_km = ?, is_active = ? WHERE id = ? '); $stmt->execute([$spz, $name, $brand ?: null, $model ?: null, $initialKm, $isActive, $id]); AuditLog::logUpdate('vehicles', $id, [], $input, 'Upraveno vozidlo'); successResponse(null, 'Vozidlo bylo aktualizováno'); } else { // Create $stmt = $pdo->prepare(' INSERT INTO vehicles (spz, name, brand, model, initial_km, is_active) VALUES (?, ?, ?, ?, ?, ?) '); try { $stmt->execute([$spz, $name, $brand ?: null, $model ?: null, $initialKm, $isActive]); $newId = (int)$pdo->lastInsertId(); AuditLog::logCreate('vehicles', $newId, $input, 'Vytvořeno vozidlo'); successResponse(['id' => $newId], 'Vozidlo bylo vytvořeno'); } catch (PDOException $e) { if ($e->getCode() == 23000) { errorResponse('Vozidlo s touto SPZ již existuje'); } throw $e; } } } // ============================================================================ // PUT Handler // ============================================================================ /** * PUT - Update trip * * @param array $authData */ function handleUpdateTrip(PDO $pdo, int $id, int $userId, array $authData): void { $stmt = $pdo->prepare( 'SELECT id, vehicle_id, user_id, trip_date, start_km, end_km, route_from, route_to, is_business, notes FROM trips WHERE id = ?' ); $stmt->execute([$id]); $trip = $stmt->fetch(); if (!$trip) { errorResponse('Záznam nebyl nalezen', 404); } // Check permission - own trips or trips.admin if ($trip['user_id'] !== $userId && !hasPermission($authData, 'trips.admin')) { errorResponse('Nemáte oprávnění upravit tento záznam', 403); } $input = getJsonInput(); $vehicleId = (int)($input['vehicle_id'] ?? $trip['vehicle_id']); $tripDate = $input['trip_date'] ?? $trip['trip_date']; $startKm = (int)($input['start_km'] ?? $trip['start_km']); $endKm = (int)($input['end_km'] ?? $trip['end_km']); $routeFrom = trim($input['route_from'] ?? $trip['route_from']); $routeTo = trim($input['route_to'] ?? $trip['route_to']); $isBusiness = isset($input['is_business']) ? (int)$input['is_business'] : $trip['is_business']; $notes = trim($input['notes'] ?? $trip['notes'] ?? ''); if ($endKm <= $startKm) { errorResponse('Konečný stav km musí být větší než počáteční'); } $stmt = $pdo->prepare(' UPDATE trips SET vehicle_id = ?, trip_date = ?, start_km = ?, end_km = ?, route_from = ?, route_to = ?, is_business = ?, notes = ? WHERE id = ? '); $stmt->execute([$vehicleId, $tripDate, $startKm, $endKm, $routeFrom, $routeTo, $isBusiness, $notes ?: null, $id]); AuditLog::logUpdate('trips', $id, $trip, $input, 'Upraven záznam jízdy'); successResponse(null, 'Záznam byl aktualizován'); } // ============================================================================ // DELETE Handlers // ============================================================================ /** * DELETE - Delete trip * * @param array $authData */ function handleDeleteTrip(PDO $pdo, int $id, int $userId, array $authData): void { $stmt = $pdo->prepare( 'SELECT id, vehicle_id, user_id, trip_date, start_km, end_km, route_from, route_to, is_business, notes FROM trips WHERE id = ?' ); $stmt->execute([$id]); $trip = $stmt->fetch(); if (!$trip) { errorResponse('Záznam nebyl nalezen', 404); } // Check permission - own trips or trips.admin if ($trip['user_id'] !== $userId && !hasPermission($authData, 'trips.admin')) { errorResponse('Nemáte oprávnění smazat tento záznam', 403); } $stmt = $pdo->prepare('DELETE FROM trips WHERE id = ?'); $stmt->execute([$id]); AuditLog::logDelete('trips', $id, $trip, 'Smazán záznam jízdy'); successResponse(null, 'Záznam byl smazán'); } /** * DELETE - Delete vehicle (admin) */ function handleDeleteVehicle(PDO $pdo, int $id): void { if (!$id) { errorResponse('ID je povinné'); } $stmt = $pdo->prepare( 'SELECT id, spz, name, brand, model, is_active FROM vehicles WHERE id = ?' ); $stmt->execute([$id]); $vehicle = $stmt->fetch(); if (!$vehicle) { errorResponse('Vozidlo nebylo nalezeno', 404); } // Check if vehicle has trips $stmt = $pdo->prepare('SELECT COUNT(*) FROM trips WHERE vehicle_id = ?'); $stmt->execute([$id]); $tripCount = $stmt->fetchColumn(); if ($tripCount > 0) { errorResponse( "Nelze smazat vozidlo s {$tripCount} záznamy jízd. Nejprve smažte záznamy jízd nebo deaktivujte vozidlo." ); } $stmt = $pdo->prepare('DELETE FROM vehicles WHERE id = ?'); $stmt->execute([$id]); AuditLog::logDelete('vehicles', $id, $vehicle, 'Smazáno vozidlo'); successResponse(null, 'Vozidlo bylo smazáno'); } // ============================================================================ // Print Handler // ============================================================================ /** * Format date range for display */ function formatPeriodName(string $startDate, string $endDate): string { $start = new DateTime($startDate); $end = new DateTime($endDate); // If same month if ($start->format('Y-m') === $end->format('Y-m')) { return getCzechMonthName((int)$start->format('n')) . ' ' . $start->format('Y'); } // If same year if ($start->format('Y') === $end->format('Y')) { return $start->format('j.n.') . ' - ' . $end->format('j.n.Y'); } // Different years return $start->format('j.n.Y') . ' - ' . $end->format('j.n.Y'); } /** * GET - Print data for trips (admin) */ function handleGetPrint(PDO $pdo): void { $dateFrom = $_GET['date_from'] ?? null; $dateTo = $_GET['date_to'] ?? null; $vehicleId = isset($_GET['vehicle_id']) && $_GET['vehicle_id'] !== '' ? (int)$_GET['vehicle_id'] : null; $filterUserId = isset($_GET['user_id']) && $_GET['user_id'] !== '' ? (int)$_GET['user_id'] : null; // Default to current month if no dates provided if (!$dateFrom || !$dateTo) { $startDate = date('Y-m-01'); $endDate = date('Y-m-t'); } else { if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) { errorResponse('Neplatný formát data (očekáváno YYYY-MM-DD)'); } $startDate = $dateFrom; $endDate = $dateTo; } $sql = " SELECT t.id, t.vehicle_id, t.user_id, t.trip_date, t.start_km, t.end_km, t.distance, t.route_from, t.route_to, t.is_business, t.notes, t.created_at, v.spz, v.name as vehicle_name, v.brand, v.model, CONCAT(u.first_name, ' ', u.last_name) as driver_name FROM trips t JOIN vehicles v ON t.vehicle_id = v.id JOIN users u ON t.user_id = u.id WHERE t.trip_date BETWEEN ? AND ? "; $params = [$startDate, $endDate]; if ($vehicleId) { $sql .= ' AND t.vehicle_id = ?'; $params[] = $vehicleId; } if ($filterUserId) { $sql .= ' AND t.user_id = ?'; $params[] = $filterUserId; } $sql .= ' ORDER BY t.trip_date ASC, t.start_km ASC'; $stmt = $pdo->prepare($sql); $stmt->execute($params); $trips = $stmt->fetchAll(); // Get vehicles for filter $stmt = $pdo->query('SELECT id, spz, name FROM vehicles ORDER BY name'); $vehicles = $stmt->fetchAll(); // Get users for filter $stmt = $pdo->query( "SELECT id, CONCAT(first_name, ' ', last_name) as name FROM users WHERE is_active = 1 ORDER BY last_name" ); $users = $stmt->fetchAll(); // Calculate totals $totalDistance = 0; $businessDistance = 0; $privateDistance = 0; foreach ($trips as $trip) { $totalDistance += $trip['distance']; if ($trip['is_business']) { $businessDistance += $trip['distance']; } else { $privateDistance += $trip['distance']; } } // Get selected vehicle/user names for header $selectedVehicleName = ''; if ($vehicleId) { $stmt = $pdo->prepare("SELECT CONCAT(spz, ' - ', name) as name FROM vehicles WHERE id = ?"); $stmt->execute([$vehicleId]); $v = $stmt->fetch(); $selectedVehicleName = $v ? $v['name'] : ''; } $selectedUserName = ''; if ($filterUserId) { $stmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) as name FROM users WHERE id = ?"); $stmt->execute([$filterUserId]); $u = $stmt->fetch(); $selectedUserName = $u ? $u['name'] : ''; } successResponse([ 'trips' => $trips, 'vehicles' => $vehicles, 'users' => $users, 'date_from' => $startDate, 'date_to' => $endDate, 'period_name' => formatPeriodName($startDate, $endDate), 'selected_vehicle' => $vehicleId, 'selected_vehicle_name' => $selectedVehicleName, 'selected_user' => $filterUserId, 'selected_user_name' => $selectedUserName, 'totals' => [ 'total' => $totalDistance, 'business' => $businessDistance, 'private' => $privateDistance, 'count' => count($trips), ], ]); }