false, 'error' => 'Authentication required']); exit; } // Check admin privileges if (!$_SESSION['user']['is_admin']) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Admin privileges required']); exit; } $currentUserId = $_SESSION['user']['user_id']; // CSRF Protection for write operations if ($_SERVER['REQUEST_METHOD'] !== 'GET') { require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php'; $csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; if (!CsrfMiddleware::validateToken($csrfToken)) { http_response_code(403); echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']); exit; } } // Use centralized database connection $conn = Database::getConnection(); header('Content-Type: application/json'); $model = new RecurringTicketModel($conn); $method = $_SERVER['REQUEST_METHOD']; $id = isset($_GET['id']) ? (int)$_GET['id'] : null; $action = isset($_GET['action']) ? $_GET['action'] : null; switch ($method) { case 'GET': if ($id) { $recurring = $model->getById($id); echo json_encode(['success' => (bool)$recurring, 'recurring' => $recurring]); } else { $all = $model->getAll(true); echo json_encode(['success' => true, 'recurring_tickets' => $all]); } break; case 'POST': if ($action === 'toggle' && $id) { $result = $model->toggleActive($id); echo json_encode($result); } else { $data = json_decode(file_get_contents('php://input'), true); if (!is_array($data) || empty($data['schedule_type']) || empty($data['title_template'])) { echo json_encode(['success' => false, 'error' => 'schedule_type and title_template are required']); exit; } // Calculate next run time $nextRun = calculateNextRun( $data['schedule_type'], $data['schedule_day'] ?? null, $data['schedule_time'] ?? '09:00' ); $data['next_run_at'] = $nextRun; $data['is_active'] = isset($data['is_active']) ? (int)$data['is_active'] : 1; $data['created_by'] = $currentUserId; $result = $model->create($data); echo json_encode($result); } break; case 'PUT': if (!$id) { echo json_encode(['success' => false, 'error' => 'ID required']); exit; } $data = json_decode(file_get_contents('php://input'), true); if (!is_array($data) || empty($data['schedule_type'])) { echo json_encode(['success' => false, 'error' => 'Invalid request data']); exit; } // Recalculate next run time if schedule changed $nextRun = calculateNextRun( $data['schedule_type'], $data['schedule_day'] ?? null, $data['schedule_time'] ?? '09:00' ); $data['next_run_at'] = $nextRun; $data['is_active'] = isset($data['is_active']) ? (int)$data['is_active'] : 1; $result = $model->update($id, $data); echo json_encode($result); break; case 'DELETE': if (!$id) { echo json_encode(['success' => false, 'error' => 'ID required']); exit; } $result = $model->delete($id); echo json_encode($result); break; default: http_response_code(405); echo json_encode(['success' => false, 'error' => 'Method not allowed']); } } catch (Exception $e) { error_log("Recurring tickets API error: " . $e->getMessage()); http_response_code(500); echo json_encode(['success' => false, 'error' => 'An internal error occurred']); } function calculateNextRun($scheduleType, $scheduleDay, $scheduleTime) { $now = new DateTime(); $time = $scheduleTime ?: '09:00'; switch ($scheduleType) { case 'daily': $next = new DateTime('tomorrow ' . $time); break; case 'weekly': $days = [1 => 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; $dayName = $days[(int)$scheduleDay] ?? 'Monday'; $next = new DateTime("next {$dayName} " . $time); break; case 'monthly': $day = max(1, min(31, (int)$scheduleDay)); $next = new DateTime(); $next->modify('first day of next month'); // Clamp to last day of target month (handles Feb, 30-day months) $daysInMonth = (int)$next->format('t'); $day = min($day, $daysInMonth); $next->setDate((int)$next->format('Y'), (int)$next->format('m'), $day); $parts = explode(':', $time . ':00'); // ensure at least H:M $next->setTime((int)$parts[0], (int)$parts[1], 0); break; default: $next = new DateTime('tomorrow ' . $time); } return $next->format('Y-m-d H:i:s'); }