- admin/quickTasks: list, create, save, delete - admin/scheduledTasks: list, save, delete, toggle, run, runDue - cron: expireStaleChats, expireTabs - receipt: public order receipt page (no auth, UUID-secured) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
127 lines
5.7 KiB
PHP
127 lines
5.7 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../../helpers.php';
|
|
require_once __DIR__ . '/_cronUtils.php';
|
|
runAuth();
|
|
|
|
/**
|
|
* Create or update a scheduled task definition.
|
|
* POST: { BusinessID, ScheduledTaskID? (for update), Name, Title, Details, CategoryID,
|
|
* CronExpression, IsActive, ScheduleType ('cron'|'interval'|'interval_after_completion'),
|
|
* IntervalMinutes (for interval types) }
|
|
*/
|
|
|
|
$data = readJsonBody();
|
|
$businessID = (int) ($data['BusinessID'] ?? headerValue('X-Business-ID') ?? 0);
|
|
$taskID = (int) ($data['ScheduledTaskID'] ?? 0);
|
|
$taskName = trim($data['Name'] ?? '');
|
|
$taskTitle = trim($data['Title'] ?? '');
|
|
$taskDetails = trim($data['Details'] ?? '');
|
|
$categoryID = (isset($data['CategoryID']) && is_numeric($data['CategoryID']) && $data['CategoryID'] > 0) ? (int) $data['CategoryID'] : null;
|
|
$cronExpression = trim($data['CronExpression'] ?? '');
|
|
$isActive = isset($data['IsActive']) ? ($data['IsActive'] ? 1 : 0) : 1;
|
|
$scheduleType = trim($data['ScheduleType'] ?? 'cron');
|
|
$intervalMinutes = (isset($data['IntervalMinutes']) && is_numeric($data['IntervalMinutes'])) ? (int) $data['IntervalMinutes'] : null;
|
|
|
|
if ($businessID <= 0) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'missing_params', 'MESSAGE' => 'BusinessID is required']);
|
|
}
|
|
if ($taskName === '') {
|
|
apiAbort(['OK' => false, 'ERROR' => 'missing_params', 'MESSAGE' => 'Name is required']);
|
|
}
|
|
if ($taskTitle === '') {
|
|
apiAbort(['OK' => false, 'ERROR' => 'missing_params', 'MESSAGE' => 'Title is required']);
|
|
}
|
|
|
|
try {
|
|
// Determine next run based on schedule type
|
|
if ($scheduleType === 'interval' || $scheduleType === 'interval_after_completion') {
|
|
if ($intervalMinutes === null || $intervalMinutes < 1) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'missing_params', 'MESSAGE' => 'IntervalMinutes is required for interval scheduling (minimum 1)']);
|
|
}
|
|
if ($cronExpression === '') {
|
|
$cronExpression = '* * * * *';
|
|
}
|
|
$nextRunOn = ($taskID === 0)
|
|
? new DateTime('now', new DateTimeZone('UTC'))
|
|
: new DateTime("+{$intervalMinutes} minutes", new DateTimeZone('UTC'));
|
|
} else {
|
|
if ($cronExpression === '') {
|
|
apiAbort(['OK' => false, 'ERROR' => 'missing_params', 'MESSAGE' => 'CronExpression is required']);
|
|
}
|
|
$cronParts = preg_split('/\s+/', $cronExpression);
|
|
if (count($cronParts) !== 5) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'invalid_cron', 'MESSAGE' => 'Cron expression must have 5 parts: minute hour day month weekday']);
|
|
}
|
|
$nextRunOn = calculateNextRun($cronExpression);
|
|
}
|
|
|
|
$nextRunStr = $nextRunOn->format('Y-m-d H:i:s');
|
|
|
|
if ($taskID > 0) {
|
|
// UPDATE
|
|
$existing = queryOne("SELECT ID FROM ScheduledTaskDefinitions WHERE ID = ? AND BusinessID = ?", [$taskID, $businessID]);
|
|
if (!$existing) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'not_found', 'MESSAGE' => 'Scheduled task not found']);
|
|
}
|
|
|
|
queryTimed("
|
|
UPDATE ScheduledTaskDefinitions SET
|
|
Name = ?, Title = ?, Details = ?, TaskCategoryID = ?,
|
|
CronExpression = ?, ScheduleType = ?, IntervalMinutes = ?,
|
|
IsActive = ?, NextRunOn = ?
|
|
WHERE ID = ?
|
|
", [$taskName, $taskTitle, $taskDetails ?: null, $categoryID, $cronExpression, $scheduleType, $intervalMinutes, $isActive, $nextRunStr, $taskID]);
|
|
|
|
jsonResponse([
|
|
'OK' => true,
|
|
'SCHEDULED_TASK_ID' => $taskID,
|
|
'NEXT_RUN' => $nextRunStr,
|
|
'MESSAGE' => 'Scheduled task updated',
|
|
]);
|
|
} else {
|
|
// INSERT
|
|
queryTimed("
|
|
INSERT INTO ScheduledTaskDefinitions (
|
|
BusinessID, Name, Title, Details, TaskCategoryID,
|
|
CronExpression, ScheduleType, IntervalMinutes, IsActive, NextRunOn
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
", [$businessID, $taskName, $taskTitle, $taskDetails ?: null, $categoryID, $cronExpression, $scheduleType, $intervalMinutes, $isActive, $nextRunStr]);
|
|
|
|
$newScheduledTaskID = (int) lastInsertId();
|
|
|
|
// Create the first task immediately
|
|
queryTimed("
|
|
INSERT INTO Tasks (BusinessID, CategoryID, TaskTypeID, Title, Details, CreatedOn, ClaimedByUserID)
|
|
VALUES (?, ?, 0, ?, ?, NOW(), 0)
|
|
", [$businessID, $categoryID, $taskTitle, $taskDetails ?: null]);
|
|
|
|
$firstTaskID = (int) lastInsertId();
|
|
|
|
// Calculate actual next run (after the immediate one)
|
|
if ($scheduleType === 'interval_after_completion') {
|
|
$actualNextRunStr = null; // Don't schedule until task completion
|
|
} elseif ($scheduleType === 'interval') {
|
|
$actualNextRun = new DateTime("+{$intervalMinutes} minutes", new DateTimeZone('UTC'));
|
|
$actualNextRunStr = $actualNextRun->format('Y-m-d H:i:s');
|
|
} else {
|
|
$actualNextRun = calculateNextRun($cronExpression);
|
|
$actualNextRunStr = $actualNextRun->format('Y-m-d H:i:s');
|
|
}
|
|
|
|
queryTimed("
|
|
UPDATE ScheduledTaskDefinitions SET LastRunOn = NOW(), NextRunOn = ?
|
|
WHERE ID = ?
|
|
", [$actualNextRunStr, $newScheduledTaskID]);
|
|
|
|
jsonResponse([
|
|
'OK' => true,
|
|
'SCHEDULED_TASK_ID' => $newScheduledTaskID,
|
|
'TASK_ID' => $firstTaskID,
|
|
'NEXT_RUN' => $actualNextRunStr ?? '',
|
|
'MESSAGE' => 'Scheduled task created and first task added',
|
|
]);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => $e->getMessage()]);
|
|
}
|