2026-01-20 09:55:01 -05:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Recurring Tickets Management API
|
|
|
|
|
* CRUD operations for recurring_tickets table
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
ini_set('display_errors', 0);
|
|
|
|
|
error_reporting(E_ALL);
|
|
|
|
|
|
|
|
|
|
require_once dirname(__DIR__) . '/middleware/RateLimitMiddleware.php';
|
|
|
|
|
RateLimitMiddleware::apply('api');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
require_once dirname(__DIR__) . '/config/config.php';
|
2026-01-30 14:39:13 -05:00
|
|
|
require_once dirname(__DIR__) . '/helpers/Database.php';
|
2026-01-20 09:55:01 -05:00
|
|
|
require_once dirname(__DIR__) . '/models/RecurringTicketModel.php';
|
|
|
|
|
|
|
|
|
|
// Check authentication
|
2026-04-05 17:52:07 -04:00
|
|
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
2026-01-20 09:55:01 -05:00
|
|
|
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
|
|
|
|
http_response_code(401);
|
|
|
|
|
echo json_encode(['success' => 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 14:39:13 -05:00
|
|
|
// Use centralized database connection
|
|
|
|
|
$conn = Database::getConnection();
|
2026-01-20 09:55:01 -05:00
|
|
|
|
|
|
|
|
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);
|
2026-03-28 22:33:48 -04:00
|
|
|
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;
|
|
|
|
|
}
|
2026-01-20 09:55:01 -05:00
|
|
|
|
|
|
|
|
// 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);
|
2026-03-28 22:33:48 -04:00
|
|
|
if (!is_array($data) || empty($data['schedule_type'])) {
|
|
|
|
|
echo json_encode(['success' => false, 'error' => 'Invalid request data']);
|
|
|
|
|
exit;
|
|
|
|
|
}
|
2026-01-20 09:55:01 -05:00
|
|
|
|
|
|
|
|
// 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) {
|
2026-01-30 18:56:29 -05:00
|
|
|
error_log("Recurring tickets API error: " . $e->getMessage());
|
2026-01-20 09:55:01 -05:00
|
|
|
http_response_code(500);
|
2026-01-30 18:56:29 -05:00
|
|
|
echo json_encode(['success' => false, 'error' => 'An internal error occurred']);
|
2026-01-20 09:55:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculateNextRun($scheduleType, $scheduleDay, $scheduleTime) {
|
|
|
|
|
$now = new DateTime();
|
|
|
|
|
$time = $scheduleTime ?: '09:00';
|
|
|
|
|
|
|
|
|
|
switch ($scheduleType) {
|
|
|
|
|
case 'daily':
|
|
|
|
|
$next = new DateTime('tomorrow ' . $time);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'weekly':
|
2026-04-05 18:16:41 -04:00
|
|
|
$days = [1 => 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
|
|
|
$dayName = $days[(int)$scheduleDay] ?? 'Monday';
|
2026-01-20 09:55:01 -05:00
|
|
|
$next = new DateTime("next {$dayName} " . $time);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'monthly':
|
2026-04-05 18:16:41 -04:00
|
|
|
$day = max(1, min(31, (int)$scheduleDay));
|
2026-01-20 09:55:01 -05:00
|
|
|
$next = new DateTime();
|
|
|
|
|
$next->modify('first day of next month');
|
2026-04-05 18:16:41 -04:00
|
|
|
// 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);
|
2026-01-20 09:55:01 -05:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
$next = new DateTime('tomorrow ' . $time);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $next->format('Y-m-d H:i:s');
|
|
|
|
|
}
|