payfrit-api/api/admin/scheduledTasks/save.php
John Mizerek 4d806d4e1e Port admin, cron, and receipt endpoints from CFML to PHP
- 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>
2026-03-14 15:57:25 -07:00

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()]);
}