1491 lines
41 KiB
PHP
1491 lines
41 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../db.php';
|
|
require_once __DIR__ . '/../auth.php';
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
function jsonResponse(array $data, int $status = 200): void
|
|
{
|
|
http_response_code($status);
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
|
header('Pragma: no-cache');
|
|
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
function getInputData(): array
|
|
{
|
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
|
|
|
if (str_contains($contentType, 'application/json')) {
|
|
$raw = file_get_contents('php://input');
|
|
$data = json_decode($raw, true);
|
|
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
return $_POST;
|
|
}
|
|
|
|
function requireProjectAccess(string $projectId, int $userId): void
|
|
{
|
|
requireProjectAccessForUser($userId, $projectId);
|
|
}
|
|
|
|
function requireTaskAccess(string $taskId, int $userId): void
|
|
{
|
|
$stmt = db()->prepare(
|
|
'SELECT tasks.project
|
|
FROM tasks
|
|
INNER JOIN projects ON projects.id = tasks.project
|
|
WHERE tasks.id = ?
|
|
LIMIT 1'
|
|
);
|
|
|
|
$stmt->execute([$taskId]);
|
|
$projectId = $stmt->fetchColumn();
|
|
|
|
if (!$projectId) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task not found.'
|
|
], 404);
|
|
}
|
|
|
|
requireProjectAccessForUser($userId, (string)$projectId);
|
|
}
|
|
|
|
function optionResponse(?array $option): ?array
|
|
{
|
|
if (!$option) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'id' => (int)$option['id'],
|
|
'name' => $option['name'],
|
|
'icon' => $option['logo'] !== null
|
|
? 'data:image/svg+xml;base64,' . base64_encode($option['logo'])
|
|
: null
|
|
];
|
|
}
|
|
|
|
function stateResponse(?array $state): ?array
|
|
{
|
|
if (!$state) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'id' => (int)$state['id'],
|
|
'name' => $state['name'],
|
|
'color' => $state['color']
|
|
];
|
|
}
|
|
|
|
function customFieldResponse(array $field): array
|
|
{
|
|
$storedValue = $field['value'] ?? $field['default_value'] ?? null;
|
|
|
|
if ($storedValue === null) {
|
|
return [
|
|
'id' => (int)$field['id'],
|
|
'task_type' => (int)$field['task_type'],
|
|
'name' => $field['name'],
|
|
'type' => $field['type'],
|
|
'value' => null,
|
|
'raw_value' => ''
|
|
];
|
|
}
|
|
|
|
$decodedValue = json_decode((string)$storedValue, true);
|
|
$decodeOk = json_last_error() === JSON_ERROR_NONE;
|
|
$rawValue = (string)$storedValue;
|
|
|
|
if ($decodeOk) {
|
|
if ($field['type'] === 'json') {
|
|
$rawValue = json_encode($decodedValue, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
} elseif ($field['type'] === 'boolean') {
|
|
$rawValue = $decodedValue ? 'true' : 'false';
|
|
} else {
|
|
$rawValue = (string)$decodedValue;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'id' => (int)$field['id'],
|
|
'task_type' => (int)$field['task_type'],
|
|
'name' => $field['name'],
|
|
'type' => $field['type'],
|
|
'value' => $decodeOk ? $decodedValue : null,
|
|
'raw_value' => $rawValue
|
|
];
|
|
}
|
|
|
|
function normalizeCustomFieldValue(mixed $value, string $type): ?string
|
|
{
|
|
$rawValue = trim((string)$value);
|
|
|
|
if ($rawValue === '') {
|
|
return null;
|
|
}
|
|
|
|
$normalized = match ($type) {
|
|
'int' => filter_var($rawValue, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE),
|
|
'float' => is_numeric($rawValue) ? (float)$rawValue : null,
|
|
'boolean' => match (strtolower($rawValue)) {
|
|
'1', 'true', 'yes', 'on' => true,
|
|
'0', 'false', 'no', 'off' => false,
|
|
default => null
|
|
},
|
|
'date' => preg_match('/^\d{4}-\d{2}-\d{2}$/', $rawValue) ? $rawValue : null,
|
|
'json' => json_decode($rawValue, true),
|
|
default => $rawValue
|
|
};
|
|
|
|
if ($type === 'json' && json_last_error() !== JSON_ERROR_NONE) {
|
|
jsonResponse(['success' => false, 'error' => 'Custom field JSON value is invalid.'], 400);
|
|
}
|
|
|
|
if ($normalized === null && $type !== 'json') {
|
|
jsonResponse(['success' => false, 'error' => "Custom field value does not match {$type}."], 400);
|
|
}
|
|
|
|
return json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
}
|
|
|
|
function ensureCustomTaskFieldsTable(): void
|
|
{
|
|
db()->exec(
|
|
'CREATE TABLE IF NOT EXISTS custom_task_fields (
|
|
id INT NOT NULL AUTO_INCREMENT,
|
|
task_type INT NOT NULL,
|
|
name VARCHAR(128) NOT NULL,
|
|
type VARCHAR(128) NOT NULL,
|
|
default_value TEXT NULL,
|
|
PRIMARY KEY (id),
|
|
INDEX idx_custom_task_fields_task_type (task_type)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
|
);
|
|
|
|
$columns = db()->query('SHOW COLUMNS FROM custom_task_fields')->fetchAll(PDO::FETCH_COLUMN);
|
|
|
|
if (!in_array('default_value', $columns, true)) {
|
|
db()->exec('ALTER TABLE custom_task_fields ADD COLUMN default_value TEXT NULL');
|
|
}
|
|
|
|
if (in_array('value', $columns, true)) {
|
|
db()->exec('UPDATE custom_task_fields SET default_value = `value` WHERE default_value IS NULL');
|
|
}
|
|
}
|
|
|
|
function ensureCustomFieldValuesTable(): void
|
|
{
|
|
db()->exec(
|
|
'CREATE TABLE IF NOT EXISTS custom_field_values (
|
|
id INT NOT NULL AUTO_INCREMENT,
|
|
field_id INT NOT NULL,
|
|
task_id VARCHAR(26) NOT NULL,
|
|
`value` TEXT NULL,
|
|
PRIMARY KEY (id),
|
|
INDEX idx_custom_field_values_task (task_id),
|
|
INDEX idx_custom_field_values_field (field_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
|
);
|
|
}
|
|
|
|
function pictureDataUri(?string $picture): ?string
|
|
{
|
|
if ($picture === null || $picture === '') {
|
|
return null;
|
|
}
|
|
|
|
$info = @getimagesizefromstring($picture);
|
|
$mime = is_array($info) && isset($info['mime']) ? $info['mime'] : 'image/png';
|
|
|
|
return 'data:' . $mime . ';base64,' . base64_encode($picture);
|
|
}
|
|
|
|
function requireTaskOption(string $table, int $id, string $label): void
|
|
{
|
|
if ($id <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => "{$label} must be valid."
|
|
], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare("SELECT id FROM {$table} WHERE id = ? LIMIT 1");
|
|
$stmt->execute([$id]);
|
|
|
|
if (!$stmt->fetch()) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => "{$label} not found."
|
|
], 400);
|
|
}
|
|
}
|
|
|
|
function requireUserExists(?int $userId): void
|
|
{
|
|
if ($userId === null) {
|
|
return;
|
|
}
|
|
|
|
$stmt = db()->prepare('SELECT id FROM users WHERE id = ? LIMIT 1');
|
|
$stmt->execute([$userId]);
|
|
|
|
if (!$stmt->fetch()) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Assignee not found.'
|
|
], 400);
|
|
}
|
|
}
|
|
|
|
function requireVersionForProject(?int $versionId, string $projectId): void
|
|
{
|
|
if ($versionId === null) {
|
|
return;
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT id
|
|
FROM versions
|
|
WHERE id = ?
|
|
AND project = ?
|
|
LIMIT 1'
|
|
);
|
|
|
|
$stmt->execute([$versionId, $projectId]);
|
|
|
|
if (!$stmt->fetch()) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Fix version not found for this project.'
|
|
], 400);
|
|
}
|
|
}
|
|
|
|
function getTaskProject(string $taskId, int $userId): string
|
|
{
|
|
$stmt = db()->prepare(
|
|
'SELECT tasks.project
|
|
FROM tasks
|
|
INNER JOIN projects ON projects.id = tasks.project
|
|
WHERE tasks.id = ?
|
|
LIMIT 1'
|
|
);
|
|
|
|
$stmt->execute([$taskId]);
|
|
$projectId = $stmt->fetchColumn();
|
|
|
|
if (!$projectId) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task not found.'
|
|
], 404);
|
|
}
|
|
|
|
return (string)$projectId;
|
|
}
|
|
|
|
function requireCommentForTask(int $commentId, string $taskId, int $userId): void
|
|
{
|
|
if ($commentId <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Parent comment must be valid.'
|
|
], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT comments.id
|
|
FROM comments
|
|
INNER JOIN tasks ON tasks.id = comments.task_id
|
|
INNER JOIN projects ON projects.id = tasks.project
|
|
WHERE comments.id = ?
|
|
AND comments.task_id = ?
|
|
LIMIT 1'
|
|
);
|
|
|
|
$stmt->execute([
|
|
$commentId,
|
|
$taskId
|
|
]);
|
|
|
|
if (!$stmt->fetch()) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Parent comment not found for this task.'
|
|
], 400);
|
|
}
|
|
}
|
|
|
|
function requireOwnCommentForTask(int $commentId, string $taskId, int $userId): array
|
|
{
|
|
if ($commentId <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Comment must be valid.'
|
|
], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT comments.id, comments.response_to, comments.task_id, comments.commenter, comments.comment
|
|
FROM comments
|
|
INNER JOIN tasks ON tasks.id = comments.task_id
|
|
INNER JOIN projects ON projects.id = tasks.project
|
|
WHERE comments.id = ?
|
|
AND comments.task_id = ?
|
|
AND comments.commenter = ?
|
|
LIMIT 1'
|
|
);
|
|
|
|
$stmt->execute([
|
|
$commentId,
|
|
$taskId,
|
|
$userId
|
|
]);
|
|
|
|
$comment = $stmt->fetch();
|
|
|
|
if (!$comment) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Comment not found or not your comment.'
|
|
], 404);
|
|
}
|
|
|
|
return $comment;
|
|
}
|
|
|
|
function generateTaskId(string $projectId): string
|
|
{
|
|
$stmt = db()->prepare(
|
|
'SELECT id
|
|
FROM tasks
|
|
WHERE project = ?
|
|
AND id LIKE ?
|
|
AND SUBSTRING(id, ?) REGEXP "^[0-9]+$"
|
|
ORDER BY CAST(SUBSTRING(id, ?) AS UNSIGNED) DESC
|
|
LIMIT 1'
|
|
);
|
|
|
|
$stmt->execute([
|
|
$projectId,
|
|
$projectId . '-%',
|
|
strlen($projectId) + 2,
|
|
strlen($projectId) + 2
|
|
]);
|
|
|
|
$lastId = $stmt->fetchColumn();
|
|
|
|
if (!$lastId) {
|
|
return $projectId . '-1';
|
|
}
|
|
|
|
$parts = explode('-', $lastId);
|
|
$lastNumber = (int)end($parts);
|
|
|
|
return $projectId . '-' . ($lastNumber + 1);
|
|
}
|
|
|
|
function resolveTaskStatus(array $task): ?int
|
|
{
|
|
if ($task['status'] !== null) {
|
|
return (int)$task['status'];
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT default_state
|
|
FROM task_types
|
|
WHERE id = ?
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute([(int)$task['type']]);
|
|
$defaultState = $stmt->fetchColumn();
|
|
|
|
if (!$defaultState) {
|
|
return null;
|
|
}
|
|
|
|
$stateId = (int)$defaultState;
|
|
$assignedStmt = db()->prepare(
|
|
'SELECT id
|
|
FROM assigned_task_states
|
|
WHERE task_type = ?
|
|
AND state = ?
|
|
LIMIT 1'
|
|
);
|
|
$assignedStmt->execute([(int)$task['type'], $stateId]);
|
|
|
|
if (!$assignedStmt->fetch()) {
|
|
return null;
|
|
}
|
|
|
|
db()->prepare('UPDATE tasks SET status = ? WHERE id = ?')->execute([$stateId, $task['id']]);
|
|
|
|
return $stateId;
|
|
}
|
|
|
|
function getTaskState(?int $stateId): ?array
|
|
{
|
|
if ($stateId === null) {
|
|
return null;
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT id, name, color
|
|
FROM task_states
|
|
WHERE id = ?
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute([$stateId]);
|
|
|
|
return stateResponse($stmt->fetch() ?: null);
|
|
}
|
|
|
|
function getTaskTransitions(int $taskType, ?int $status): array
|
|
{
|
|
if ($status === null) {
|
|
return [];
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT
|
|
task_state_transitions.id,
|
|
task_state_transitions.from_id,
|
|
task_state_transitions.to_id,
|
|
task_state_transitions.action_name,
|
|
to_state.name AS to_name,
|
|
to_state.color AS to_color
|
|
FROM task_state_transitions
|
|
INNER JOIN assigned_task_states from_assignment
|
|
ON from_assignment.task_type = ?
|
|
AND from_assignment.state = task_state_transitions.from_id
|
|
INNER JOIN assigned_task_states to_assignment
|
|
ON to_assignment.task_type = ?
|
|
AND to_assignment.state = task_state_transitions.to_id
|
|
INNER JOIN task_states to_state ON to_state.id = task_state_transitions.to_id
|
|
WHERE task_state_transitions.from_id = ?
|
|
ORDER BY task_state_transitions.id ASC'
|
|
);
|
|
$stmt->execute([$taskType, $taskType, $status]);
|
|
|
|
return array_map(static function (array $transition): array {
|
|
return [
|
|
'id' => (int)$transition['id'],
|
|
'from_id' => (int)$transition['from_id'],
|
|
'to_id' => (int)$transition['to_id'],
|
|
'action_name' => $transition['action_name'],
|
|
'to_state' => [
|
|
'id' => (int)$transition['to_id'],
|
|
'name' => $transition['to_name'],
|
|
'color' => $transition['to_color']
|
|
]
|
|
];
|
|
}, $stmt->fetchAll());
|
|
}
|
|
|
|
function getTaskCustomFields(string $taskId, int $taskType): array
|
|
{
|
|
ensureCustomTaskFieldsTable();
|
|
ensureCustomFieldValuesTable();
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT
|
|
custom_task_fields.id,
|
|
custom_task_fields.task_type,
|
|
custom_task_fields.name,
|
|
custom_task_fields.type,
|
|
custom_task_fields.default_value,
|
|
custom_field_values.id AS value_id,
|
|
custom_field_values.`value`,
|
|
custom_field_values.id IS NOT NULL AS has_task_value
|
|
FROM custom_task_fields
|
|
LEFT JOIN custom_field_values
|
|
ON custom_field_values.field_id = custom_task_fields.id
|
|
AND custom_field_values.task_id = ?
|
|
WHERE custom_task_fields.task_type = ?
|
|
ORDER BY custom_task_fields.id ASC'
|
|
);
|
|
$stmt->execute([$taskId, $taskType]);
|
|
|
|
return array_map(static function (array $field): array {
|
|
$response = customFieldResponse([
|
|
'id' => $field['id'],
|
|
'task_type' => $field['task_type'],
|
|
'name' => $field['name'],
|
|
'type' => $field['type'],
|
|
'value' => $field['has_task_value'] ? $field['value'] : $field['default_value']
|
|
]);
|
|
$response['value_id'] = $field['value_id'] !== null ? (int)$field['value_id'] : null;
|
|
$response['has_task_value'] = (bool)$field['has_task_value'];
|
|
|
|
return $response;
|
|
}, $stmt->fetchAll());
|
|
}
|
|
|
|
function getCustomFieldForTask(int $fieldId, string $taskId): array
|
|
{
|
|
ensureCustomTaskFieldsTable();
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT
|
|
custom_task_fields.id,
|
|
custom_task_fields.task_type,
|
|
custom_task_fields.name,
|
|
custom_task_fields.type,
|
|
custom_task_fields.default_value,
|
|
tasks.id AS task_id
|
|
FROM custom_task_fields
|
|
INNER JOIN tasks ON tasks.type = custom_task_fields.task_type
|
|
WHERE custom_task_fields.id = ?
|
|
AND tasks.id = ?
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute([$fieldId, $taskId]);
|
|
$field = $stmt->fetch();
|
|
|
|
if (!$field) {
|
|
jsonResponse(['success' => false, 'error' => 'Custom field not found for this task.'], 404);
|
|
}
|
|
|
|
return $field;
|
|
}
|
|
|
|
function getTaskListPaging(): array
|
|
{
|
|
$page = max(1, (int)($_GET['page_number'] ?? $_GET['page'] ?? 1));
|
|
$perPage = (int)($_GET['per_page'] ?? 100);
|
|
$perPage = max(1, min(100, $perPage));
|
|
|
|
return [$page, $perPage, ($page - 1) * $perPage];
|
|
}
|
|
|
|
function getTaskListOrder(): string
|
|
{
|
|
$sort = (string)($_GET['sort'] ?? 'id');
|
|
$direction = strtolower((string)($_GET['direction'] ?? 'asc')) === 'desc' ? 'DESC' : 'ASC';
|
|
$columns = [
|
|
'id' => 'tasks.id',
|
|
'title' => 'tasks.title',
|
|
'type' => 'type_option.name',
|
|
'priority' => 'priority_option.name',
|
|
'assignee' => 'users.name',
|
|
'status' => 'status_state.name'
|
|
];
|
|
|
|
$column = $columns[$sort] ?? $columns['id'];
|
|
|
|
return "{$column} {$direction}, tasks.id ASC";
|
|
}
|
|
|
|
function taskListResponse(string $whereSql, array $params): array
|
|
{
|
|
[$page, $perPage, $offset] = getTaskListPaging();
|
|
$orderBy = getTaskListOrder();
|
|
|
|
$countStmt = db()->prepare(
|
|
"SELECT COUNT(*)
|
|
FROM tasks
|
|
INNER JOIN projects ON projects.id = tasks.project
|
|
{$whereSql}"
|
|
);
|
|
$countStmt->execute($params);
|
|
$total = (int)$countStmt->fetchColumn();
|
|
|
|
$stmt = db()->prepare(
|
|
"SELECT
|
|
tasks.id,
|
|
tasks.title,
|
|
tasks.type,
|
|
tasks.priority,
|
|
tasks.assignee,
|
|
users.name AS assignee_name,
|
|
users.picture AS assignee_picture,
|
|
COALESCE(tasks.status, task_types.default_state) AS status,
|
|
status_state.name AS status_name,
|
|
status_state.color AS status_color
|
|
FROM tasks
|
|
INNER JOIN projects ON projects.id = tasks.project
|
|
INNER JOIN task_types ON task_types.id = tasks.type
|
|
LEFT JOIN task_types type_option ON type_option.id = tasks.type
|
|
LEFT JOIN task_priorities priority_option ON priority_option.id = tasks.priority
|
|
LEFT JOIN users ON users.id = tasks.assignee
|
|
LEFT JOIN task_states status_state ON status_state.id = COALESCE(tasks.status, task_types.default_state)
|
|
{$whereSql}
|
|
ORDER BY {$orderBy}
|
|
LIMIT {$perPage} OFFSET {$offset}"
|
|
);
|
|
$stmt->execute($params);
|
|
|
|
$tasks = array_map(static function (array $task): array {
|
|
$task['assignee_picture'] = pictureDataUri($task['assignee_picture']);
|
|
$task['status'] = $task['status'] !== null ? (int)$task['status'] : null;
|
|
$task['status_state'] = $task['status'] !== null
|
|
? [
|
|
'id' => (int)$task['status'],
|
|
'name' => $task['status_name'],
|
|
'color' => $task['status_color']
|
|
]
|
|
: null;
|
|
unset($task['status_name'], $task['status_color']);
|
|
|
|
return $task;
|
|
}, $stmt->fetchAll());
|
|
|
|
return [
|
|
'tasks' => $tasks,
|
|
'pagination' => [
|
|
'page' => $page,
|
|
'per_page' => $perPage,
|
|
'total' => $total,
|
|
'total_pages' => max(1, (int)ceil($total / $perPage))
|
|
]
|
|
];
|
|
}
|
|
|
|
$user = requireApiAuth();
|
|
|
|
$api = $_GET['api'] ?? '';
|
|
|
|
switch ($api) {
|
|
|
|
case 'ListTypes': {
|
|
$stmt = db()->query(
|
|
'SELECT id, name, logo
|
|
FROM task_types
|
|
ORDER BY id ASC'
|
|
);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'types' => array_map('optionResponse', $stmt->fetchAll())
|
|
]);
|
|
}
|
|
|
|
case 'ListPriorities': {
|
|
$stmt = db()->query(
|
|
'SELECT id, name, logo
|
|
FROM task_priorities
|
|
ORDER BY id ASC'
|
|
);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'priorities' => array_map('optionResponse', $stmt->fetchAll())
|
|
]);
|
|
}
|
|
|
|
case 'TaskInfo': {
|
|
$taskId = strtoupper(trim($_GET['task_id'] ?? ''));
|
|
|
|
if ($taskId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing task_id.'
|
|
], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT
|
|
tasks.id,
|
|
tasks.title,
|
|
tasks.description,
|
|
tasks.project,
|
|
tasks.created_date,
|
|
tasks.last_changed,
|
|
tasks.reporter,
|
|
tasks.assignee,
|
|
tasks.fix_version,
|
|
tasks.type,
|
|
tasks.priority,
|
|
tasks.status
|
|
FROM tasks
|
|
INNER JOIN projects ON projects.id = tasks.project
|
|
WHERE tasks.id = ?
|
|
LIMIT 1'
|
|
);
|
|
|
|
$stmt->execute([$taskId]);
|
|
|
|
$task = $stmt->fetch();
|
|
|
|
if (!$task) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task not found.'
|
|
], 404);
|
|
}
|
|
|
|
$task['status'] = resolveTaskStatus($task);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'task' => [
|
|
...$task,
|
|
'status_state' => getTaskState($task['status']),
|
|
'status_transitions' => getTaskTransitions((int)$task['type'], $task['status']),
|
|
'custom_fields' => getTaskCustomFields((string)$task['id'], (int)$task['type'])
|
|
]
|
|
]);
|
|
}
|
|
|
|
case 'ListTasksByProject': {
|
|
$projectId = strtoupper(trim($_GET['project_id'] ?? ''));
|
|
|
|
if ($projectId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing project_id.'
|
|
], 400);
|
|
}
|
|
|
|
requireProjectAccess($projectId, (int)$user['id']);
|
|
|
|
$result = taskListResponse(
|
|
'WHERE tasks.project = ?',
|
|
[$projectId]
|
|
);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'tasks' => $result['tasks'],
|
|
'pagination' => $result['pagination']
|
|
]);
|
|
}
|
|
|
|
case 'ListTasksByVersion': {
|
|
$versionId = (int)($_GET['version_id'] ?? 0);
|
|
|
|
if ($versionId <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing or invalid version_id.'
|
|
], 400);
|
|
}
|
|
|
|
$versionProjectStmt = db()->prepare(
|
|
'SELECT project
|
|
FROM versions
|
|
WHERE id = ?
|
|
LIMIT 1'
|
|
);
|
|
$versionProjectStmt->execute([$versionId]);
|
|
$versionProject = $versionProjectStmt->fetchColumn();
|
|
|
|
if (!$versionProject) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Version not found.'
|
|
], 404);
|
|
}
|
|
|
|
requireProjectAccessForUser((int)$user['id'], (string)$versionProject);
|
|
|
|
$result = taskListResponse(
|
|
'WHERE tasks.fix_version = ?',
|
|
[$versionId]
|
|
);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'tasks' => $result['tasks'],
|
|
'pagination' => $result['pagination']
|
|
]);
|
|
}
|
|
|
|
case 'ListComments': {
|
|
$taskId = strtoupper(trim($_GET['task_id'] ?? ''));
|
|
|
|
if ($taskId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing task_id.'
|
|
], 400);
|
|
}
|
|
|
|
requireTaskAccess($taskId, (int)$user['id']);
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT
|
|
comments.id,
|
|
comments.response_to,
|
|
comments.task_id,
|
|
comments.commenter,
|
|
comments.comment,
|
|
users.name AS commenter_name,
|
|
users.email AS commenter_email,
|
|
users.picture AS commenter_picture
|
|
FROM comments
|
|
INNER JOIN users ON users.id = comments.commenter
|
|
WHERE comments.task_id = ?
|
|
ORDER BY comments.id ASC'
|
|
);
|
|
|
|
$stmt->execute([$taskId]);
|
|
|
|
$comments = array_map(static function (array $comment): array {
|
|
$comment['id'] = (int)$comment['id'];
|
|
$comment['response_to'] = $comment['response_to'] !== null
|
|
? (int)$comment['response_to']
|
|
: null;
|
|
$comment['commenter'] = (int)$comment['commenter'];
|
|
$comment['commenter_picture'] = pictureDataUri($comment['commenter_picture']);
|
|
|
|
return $comment;
|
|
}, $stmt->fetchAll());
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'comments' => $comments
|
|
]);
|
|
}
|
|
|
|
case 'Create': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Create requires POST.'
|
|
], 405);
|
|
}
|
|
|
|
$data = getInputData();
|
|
|
|
$projectId = strtoupper(trim($data['project_id'] ?? $data['project'] ?? ''));
|
|
$title = trim($data['title'] ?? '');
|
|
$description = array_key_exists('description', $data) ? trim((string)$data['description']) : null;
|
|
|
|
$assignee = isset($data['assignee']) && $data['assignee'] !== ''
|
|
? (int)$data['assignee']
|
|
: null;
|
|
|
|
$fixVersion = isset($data['fix_version']) && $data['fix_version'] !== ''
|
|
? (int)$data['fix_version']
|
|
: null;
|
|
|
|
$type = (int)($data['type'] ?? 0);
|
|
$priority = (int)($data['priority'] ?? 0);
|
|
|
|
if ($projectId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Project id is required.'
|
|
], 400);
|
|
}
|
|
|
|
requireProjectRight((int)$user['id'], $projectId, 'Create Tasks');
|
|
|
|
if ($title === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task title is required.'
|
|
], 400);
|
|
}
|
|
|
|
if ($type <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task type is required.'
|
|
], 400);
|
|
}
|
|
|
|
if ($priority <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task priority is required.'
|
|
], 400);
|
|
}
|
|
|
|
requireTaskOption('task_types', $type, 'Task type');
|
|
requireTaskOption('task_priorities', $priority, 'Task priority');
|
|
requireUserExists($assignee);
|
|
requireVersionForProject($fixVersion, $projectId);
|
|
|
|
$taskId = generateTaskId($projectId);
|
|
|
|
$stmt = db()->prepare(
|
|
'INSERT INTO tasks
|
|
(
|
|
id,
|
|
title,
|
|
description,
|
|
project,
|
|
created_date,
|
|
last_changed,
|
|
reporter,
|
|
assignee,
|
|
fix_version,
|
|
type,
|
|
priority
|
|
)
|
|
VALUES
|
|
(
|
|
?,
|
|
?,
|
|
?,
|
|
?,
|
|
CURDATE(),
|
|
CURDATE(),
|
|
?,
|
|
?,
|
|
?,
|
|
?,
|
|
?
|
|
)'
|
|
);
|
|
|
|
$stmt->execute([
|
|
$taskId,
|
|
$title,
|
|
$description !== '' ? $description : null,
|
|
$projectId,
|
|
(int)$user['id'],
|
|
$assignee,
|
|
$fixVersion,
|
|
$type,
|
|
$priority
|
|
]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'task_id' => $taskId
|
|
], 201);
|
|
}
|
|
|
|
case 'CreateComment': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'CreateComment requires POST.'
|
|
], 405);
|
|
}
|
|
|
|
$taskId = strtoupper(trim($_GET['task_id'] ?? ''));
|
|
|
|
if ($taskId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing task_id.'
|
|
], 400);
|
|
}
|
|
|
|
requireTaskAccess($taskId, (int)$user['id']);
|
|
|
|
$data = getInputData();
|
|
$comment = trim((string)($data['comment'] ?? ''));
|
|
$responseTo = isset($data['response_to']) && $data['response_to'] !== ''
|
|
? (int)$data['response_to']
|
|
: null;
|
|
|
|
if ($comment === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Comment cannot be empty.'
|
|
], 400);
|
|
}
|
|
|
|
if ($responseTo !== null) {
|
|
requireCommentForTask($responseTo, $taskId, (int)$user['id']);
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'INSERT INTO comments (response_to, task_id, commenter, comment)
|
|
VALUES (?, ?, ?, ?)'
|
|
);
|
|
|
|
$stmt->execute([
|
|
$responseTo,
|
|
$taskId,
|
|
(int)$user['id'],
|
|
$comment
|
|
]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'comment_id' => (int)db()->lastInsertId()
|
|
], 201);
|
|
}
|
|
|
|
case 'EditComment': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'EditComment requires POST.'
|
|
], 405);
|
|
}
|
|
|
|
$taskId = strtoupper(trim($_GET['task_id'] ?? ''));
|
|
$commentId = (int)($_GET['comment_id'] ?? 0);
|
|
|
|
if ($taskId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing task_id.'
|
|
], 400);
|
|
}
|
|
|
|
requireOwnCommentForTask($commentId, $taskId, (int)$user['id']);
|
|
|
|
$data = getInputData();
|
|
$comment = trim((string)($data['comment'] ?? ''));
|
|
|
|
if ($comment === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Comment cannot be empty.'
|
|
], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'UPDATE comments
|
|
SET comment = ?
|
|
WHERE id = ?
|
|
AND task_id = ?
|
|
AND commenter = ?'
|
|
);
|
|
|
|
$stmt->execute([
|
|
$comment,
|
|
$commentId,
|
|
$taskId,
|
|
(int)$user['id']
|
|
]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'comment_id' => $commentId
|
|
]);
|
|
}
|
|
|
|
case 'DeleteComment': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'DeleteComment requires POST.'
|
|
], 405);
|
|
}
|
|
|
|
$taskId = strtoupper(trim($_GET['task_id'] ?? ''));
|
|
$commentId = (int)($_GET['comment_id'] ?? 0);
|
|
|
|
if ($taskId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing task_id.'
|
|
], 400);
|
|
}
|
|
|
|
$comment = requireOwnCommentForTask($commentId, $taskId, (int)$user['id']);
|
|
$parentId = $comment['response_to'] !== null ? (int)$comment['response_to'] : null;
|
|
|
|
db()->beginTransaction();
|
|
|
|
try {
|
|
$childStmt = db()->prepare(
|
|
'UPDATE comments
|
|
SET response_to = ?
|
|
WHERE task_id = ?
|
|
AND response_to = ?'
|
|
);
|
|
$childStmt->execute([
|
|
$parentId,
|
|
$taskId,
|
|
$commentId
|
|
]);
|
|
|
|
$deleteStmt = db()->prepare(
|
|
'DELETE FROM comments
|
|
WHERE id = ?
|
|
AND task_id = ?
|
|
AND commenter = ?'
|
|
);
|
|
$deleteStmt->execute([
|
|
$commentId,
|
|
$taskId,
|
|
(int)$user['id']
|
|
]);
|
|
|
|
db()->commit();
|
|
|
|
} catch (Throwable $e) {
|
|
db()->rollBack();
|
|
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Could not delete comment.'
|
|
], 500);
|
|
}
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'comment_id' => $commentId
|
|
]);
|
|
}
|
|
|
|
case 'SetCustomFieldValue': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'SetCustomFieldValue requires POST.'
|
|
], 405);
|
|
}
|
|
|
|
$taskId = strtoupper(trim($_GET['task_id'] ?? ''));
|
|
|
|
if ($taskId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing task_id.'
|
|
], 400);
|
|
}
|
|
|
|
requireTaskAccess($taskId, (int)$user['id']);
|
|
$taskProject = getTaskProject($taskId, (int)$user['id']);
|
|
requireProjectRight((int)$user['id'], $taskProject, 'Edit Tasks');
|
|
|
|
$data = getInputData();
|
|
$fieldId = (int)($data['field_id'] ?? 0);
|
|
$field = getCustomFieldForTask($fieldId, $taskId);
|
|
$value = normalizeCustomFieldValue($data['value'] ?? '', (string)$field['type']);
|
|
|
|
ensureCustomFieldValuesTable();
|
|
|
|
$existingStmt = db()->prepare(
|
|
'SELECT id
|
|
FROM custom_field_values
|
|
WHERE field_id = ?
|
|
AND task_id = ?
|
|
ORDER BY id ASC
|
|
LIMIT 1'
|
|
);
|
|
$existingStmt->execute([$fieldId, $taskId]);
|
|
$existingId = $existingStmt->fetchColumn();
|
|
|
|
if ($existingId) {
|
|
$stmt = db()->prepare('UPDATE custom_field_values SET `value` = ? WHERE id = ?');
|
|
$stmt->execute([$value, $existingId]);
|
|
} else {
|
|
$stmt = db()->prepare(
|
|
'INSERT INTO custom_field_values (field_id, task_id, `value`)
|
|
VALUES (?, ?, ?)'
|
|
);
|
|
$stmt->execute([$fieldId, $taskId, $value]);
|
|
}
|
|
|
|
db()->prepare('UPDATE tasks SET last_changed = CURDATE() WHERE id = ?')->execute([$taskId]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'field_id' => $fieldId,
|
|
'task_id' => $taskId
|
|
]);
|
|
}
|
|
|
|
case 'TransitionStatus': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'TransitionStatus requires POST.'
|
|
], 405);
|
|
}
|
|
|
|
$taskId = strtoupper(trim($_GET['task_id'] ?? ''));
|
|
|
|
if ($taskId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing task_id.'
|
|
], 400);
|
|
}
|
|
|
|
requireTaskAccess($taskId, (int)$user['id']);
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT id, type, assignee, status
|
|
FROM tasks
|
|
WHERE id = ?
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute([$taskId]);
|
|
$task = $stmt->fetch();
|
|
|
|
if (!$task) {
|
|
jsonResponse(['success' => false, 'error' => 'Task not found.'], 404);
|
|
}
|
|
|
|
$canTransition = userIsAdmin((int)$user['id'])
|
|
|| userHasRight((int)$user['id'], 'Edit Tasks')
|
|
|| (int)$task['assignee'] === (int)$user['id'];
|
|
|
|
if (!$canTransition) {
|
|
jsonResponse(['success' => false, 'error' => 'Only the current assignee or users with Edit Tasks can transition this task.'], 403);
|
|
}
|
|
|
|
$currentStatus = resolveTaskStatus($task);
|
|
|
|
if ($currentStatus === null) {
|
|
jsonResponse(['success' => false, 'error' => 'Task has no current status.'], 400);
|
|
}
|
|
|
|
$data = getInputData();
|
|
$transitionId = (int)($data['transition_id'] ?? 0);
|
|
|
|
if ($transitionId <= 0) {
|
|
jsonResponse(['success' => false, 'error' => 'Transition must be valid.'], 400);
|
|
}
|
|
|
|
$transitionStmt = db()->prepare(
|
|
'SELECT task_state_transitions.id, task_state_transitions.from_id, task_state_transitions.to_id
|
|
FROM task_state_transitions
|
|
INNER JOIN assigned_task_states from_assignment
|
|
ON from_assignment.task_type = ?
|
|
AND from_assignment.state = task_state_transitions.from_id
|
|
INNER JOIN assigned_task_states to_assignment
|
|
ON to_assignment.task_type = ?
|
|
AND to_assignment.state = task_state_transitions.to_id
|
|
WHERE task_state_transitions.id = ?
|
|
AND task_state_transitions.from_id = ?
|
|
LIMIT 1'
|
|
);
|
|
$transitionStmt->execute([
|
|
(int)$task['type'],
|
|
(int)$task['type'],
|
|
$transitionId,
|
|
$currentStatus
|
|
]);
|
|
$transition = $transitionStmt->fetch();
|
|
|
|
if (!$transition) {
|
|
jsonResponse(['success' => false, 'error' => 'Transition is not valid for this task.'], 400);
|
|
}
|
|
|
|
db()->prepare('UPDATE tasks SET status = ?, last_changed = CURDATE() WHERE id = ?')->execute([
|
|
(int)$transition['to_id'],
|
|
$taskId
|
|
]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'task_id' => $taskId,
|
|
'status' => (int)$transition['to_id']
|
|
]);
|
|
}
|
|
|
|
case 'Edit': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Edit requires POST.'
|
|
], 405);
|
|
}
|
|
|
|
$taskId = strtoupper(trim($_GET['task_id'] ?? ''));
|
|
|
|
if ($taskId === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing task_id.'
|
|
], 400);
|
|
}
|
|
|
|
$taskProject = getTaskProject($taskId, (int)$user['id']);
|
|
requireProjectRight((int)$user['id'], $taskProject, 'Edit Tasks');
|
|
|
|
$data = getInputData();
|
|
|
|
$fields = [];
|
|
$values = [];
|
|
|
|
if (array_key_exists('title', $data)) {
|
|
$title = trim((string)$data['title']);
|
|
|
|
if ($title === '') {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task title cannot be empty.'
|
|
], 400);
|
|
}
|
|
|
|
$fields[] = 'title = ?';
|
|
$values[] = $title;
|
|
}
|
|
|
|
if (array_key_exists('description', $data)) {
|
|
$description = trim((string)$data['description']);
|
|
|
|
$fields[] = 'description = ?';
|
|
$values[] = $description !== '' ? $description : null;
|
|
}
|
|
|
|
if (array_key_exists('assignee', $data)) {
|
|
$assignee = $data['assignee'] !== '' && $data['assignee'] !== null
|
|
? (int)$data['assignee']
|
|
: null;
|
|
|
|
requireUserExists($assignee);
|
|
|
|
$fields[] = 'assignee = ?';
|
|
$values[] = $assignee;
|
|
}
|
|
|
|
if (array_key_exists('fix_version', $data)) {
|
|
$fixVersion = $data['fix_version'] !== '' && $data['fix_version'] !== null
|
|
? (int)$data['fix_version']
|
|
: null;
|
|
|
|
requireVersionForProject($fixVersion, $taskProject);
|
|
|
|
$fields[] = 'fix_version = ?';
|
|
$values[] = $fixVersion;
|
|
}
|
|
|
|
if (array_key_exists('type', $data)) {
|
|
$type = (int)$data['type'];
|
|
|
|
if ($type <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task type must be valid.'
|
|
], 400);
|
|
}
|
|
|
|
requireTaskOption('task_types', $type, 'Task type');
|
|
|
|
$defaultStateStmt = db()->prepare(
|
|
'SELECT default_state
|
|
FROM task_types
|
|
WHERE id = ?
|
|
LIMIT 1'
|
|
);
|
|
$defaultStateStmt->execute([$type]);
|
|
$defaultState = $defaultStateStmt->fetchColumn();
|
|
|
|
$fields[] = 'type = ?';
|
|
$values[] = $type;
|
|
$fields[] = 'status = ?';
|
|
$values[] = $defaultState ? (int)$defaultState : null;
|
|
|
|
ensureCustomFieldValuesTable();
|
|
db()->prepare('DELETE FROM custom_field_values WHERE task_id = ?')->execute([$taskId]);
|
|
}
|
|
|
|
if (array_key_exists('priority', $data)) {
|
|
$priority = (int)$data['priority'];
|
|
|
|
if ($priority <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task priority must be valid.'
|
|
], 400);
|
|
}
|
|
|
|
requireTaskOption('task_priorities', $priority, 'Task priority');
|
|
|
|
$fields[] = 'priority = ?';
|
|
$values[] = $priority;
|
|
}
|
|
|
|
if (empty($fields)) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'No editable fields provided.'
|
|
], 400);
|
|
}
|
|
|
|
$fields[] = 'last_changed = CURDATE()';
|
|
|
|
$values[] = $taskId;
|
|
|
|
$stmt = db()->prepare(
|
|
'UPDATE tasks
|
|
SET ' . implode(', ', $fields) . '
|
|
WHERE id = ?'
|
|
);
|
|
|
|
$stmt->execute($values);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'task_id' => $taskId
|
|
]);
|
|
}
|
|
case 'GetType': {
|
|
$typeId = (int)($_GET['type_id'] ?? 0);
|
|
|
|
if ($typeId <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing or invalid type_id.'
|
|
], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT id, name, logo
|
|
FROM task_types
|
|
WHERE id = ?
|
|
LIMIT 1'
|
|
);
|
|
|
|
$stmt->execute([$typeId]);
|
|
$type = $stmt->fetch();
|
|
|
|
if (!$type) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task type not found.'
|
|
], 404);
|
|
}
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'type' => optionResponse($type)
|
|
]);
|
|
}
|
|
|
|
case 'GetPriority': {
|
|
$priorityId = (int)($_GET['priority_id'] ?? 0);
|
|
|
|
if ($priorityId <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Missing or invalid priority_id.'
|
|
], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT id, name, logo
|
|
FROM task_priorities
|
|
WHERE id = ?
|
|
LIMIT 1'
|
|
);
|
|
|
|
$stmt->execute([$priorityId]);
|
|
$priority = $stmt->fetch();
|
|
|
|
if (!$priority) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task priority not found.'
|
|
], 404);
|
|
}
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'priority' => optionResponse($priority)
|
|
]);
|
|
}
|
|
|
|
default: {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Unknown API action.'
|
|
], 404);
|
|
}
|
|
}
|