410 lines
12 KiB
PHP
410 lines
12 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 normalizeColor(mixed $color): string
|
|
{
|
|
$color = trim((string)$color);
|
|
|
|
if ($color === '') {
|
|
return '#4ea1ff';
|
|
}
|
|
|
|
if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Color must be a hex color like #4ea1ff.'
|
|
], 400);
|
|
}
|
|
|
|
return strtolower($color);
|
|
}
|
|
|
|
function requireState(int $stateId, string $label = 'State'): void
|
|
{
|
|
if ($stateId <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => "{$label} must be valid."
|
|
], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare('SELECT id FROM task_states WHERE id = ? LIMIT 1');
|
|
$stmt->execute([$stateId]);
|
|
|
|
if (!$stmt->fetch()) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => "{$label} not found."
|
|
], 400);
|
|
}
|
|
}
|
|
|
|
function requireTaskType(int $taskTypeId): void
|
|
{
|
|
if ($taskTypeId <= 0) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task type must be valid.'
|
|
], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare('SELECT id FROM task_types WHERE id = ? LIMIT 1');
|
|
$stmt->execute([$taskTypeId]);
|
|
|
|
if (!$stmt->fetch()) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Task type not found.'
|
|
], 400);
|
|
}
|
|
}
|
|
|
|
function optionResponse(array $option): array
|
|
{
|
|
return [
|
|
'id' => (int)$option['id'],
|
|
'name' => $option['name'],
|
|
'default_state' => isset($option['default_state']) && $option['default_state'] !== null
|
|
? (int)$option['default_state']
|
|
: null,
|
|
'icon' => $option['logo'] !== null
|
|
? 'data:image/svg+xml;base64,' . base64_encode($option['logo'])
|
|
: null
|
|
];
|
|
}
|
|
|
|
function workflowData(): array
|
|
{
|
|
$states = array_map(static function (array $state): array {
|
|
return [
|
|
'id' => (int)$state['id'],
|
|
'name' => $state['name'],
|
|
'color' => $state['color']
|
|
];
|
|
}, db()->query(
|
|
'SELECT id, name, color
|
|
FROM task_states
|
|
ORDER BY id ASC'
|
|
)->fetchAll());
|
|
|
|
$transitions = 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']
|
|
];
|
|
}, db()->query(
|
|
'SELECT id, from_id, to_id, action_name
|
|
FROM task_state_transitions
|
|
ORDER BY id ASC'
|
|
)->fetchAll());
|
|
|
|
$taskTypes = array_map('optionResponse', db()->query(
|
|
'SELECT id, name, logo, default_state
|
|
FROM task_types
|
|
ORDER BY id ASC'
|
|
)->fetchAll());
|
|
|
|
$assignments = array_map(static function (array $assignment): array {
|
|
return [
|
|
'id' => (int)$assignment['id'],
|
|
'task_type' => (int)$assignment['task_type'],
|
|
'state' => (int)$assignment['state']
|
|
];
|
|
}, db()->query(
|
|
'SELECT id, task_type, state
|
|
FROM assigned_task_states
|
|
ORDER BY task_type ASC, state ASC, id ASC'
|
|
)->fetchAll());
|
|
|
|
return [
|
|
'states' => $states,
|
|
'transitions' => $transitions,
|
|
'task_types' => $taskTypes,
|
|
'assignments' => $assignments
|
|
];
|
|
}
|
|
|
|
$user = requireApiAuth();
|
|
requireUserRight((int)$user['id'], 'Admin');
|
|
|
|
$api = $_GET['api'] ?? '';
|
|
|
|
switch ($api) {
|
|
case 'WorkflowData': {
|
|
jsonResponse([
|
|
'success' => true,
|
|
'workflow' => workflowData()
|
|
]);
|
|
}
|
|
|
|
case 'CreateState': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse(['success' => false, 'error' => 'CreateState requires POST.'], 405);
|
|
}
|
|
|
|
$data = getInputData();
|
|
$name = trim((string)($data['name'] ?? ''));
|
|
$color = normalizeColor($data['color'] ?? '');
|
|
|
|
if ($name === '') {
|
|
jsonResponse(['success' => false, 'error' => 'State name is required.'], 400);
|
|
}
|
|
|
|
if (strlen($name) > 128) {
|
|
jsonResponse(['success' => false, 'error' => 'State name is too long.'], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare('INSERT INTO task_states (name, color) VALUES (?, ?)');
|
|
$stmt->execute([$name, $color]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'state_id' => (int)db()->lastInsertId()
|
|
], 201);
|
|
}
|
|
|
|
case 'DeleteState': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse(['success' => false, 'error' => 'DeleteState requires POST.'], 405);
|
|
}
|
|
|
|
$stateId = (int)($_GET['state_id'] ?? 0);
|
|
requireState($stateId);
|
|
|
|
try {
|
|
db()->beginTransaction();
|
|
db()->prepare('DELETE FROM assigned_task_states WHERE state = ?')->execute([$stateId]);
|
|
db()->prepare('DELETE FROM task_state_transitions WHERE from_id = ? OR to_id = ?')->execute([$stateId, $stateId]);
|
|
db()->prepare('UPDATE task_types SET default_state = NULL WHERE default_state = ?')->execute([$stateId]);
|
|
db()->prepare('DELETE FROM task_states WHERE id = ?')->execute([$stateId]);
|
|
db()->commit();
|
|
|
|
} catch (Throwable $e) {
|
|
db()->rollBack();
|
|
jsonResponse(['success' => false, 'error' => 'Could not delete state.'], 500);
|
|
}
|
|
|
|
jsonResponse(['success' => true, 'state_id' => $stateId]);
|
|
}
|
|
|
|
case 'CreateTransition': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse(['success' => false, 'error' => 'CreateTransition requires POST.'], 405);
|
|
}
|
|
|
|
$data = getInputData();
|
|
$fromId = (int)($data['from_id'] ?? 0);
|
|
$toId = (int)($data['to_id'] ?? 0);
|
|
$actionName = trim((string)($data['action_name'] ?? ''));
|
|
|
|
requireState($fromId, 'From state');
|
|
requireState($toId, 'To state');
|
|
|
|
if ($actionName === '') {
|
|
jsonResponse(['success' => false, 'error' => 'Action name is required.'], 400);
|
|
}
|
|
|
|
if (strlen($actionName) > 128) {
|
|
jsonResponse(['success' => false, 'error' => 'Action name is too long.'], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'INSERT INTO task_state_transitions (from_id, to_id, action_name)
|
|
VALUES (?, ?, ?)'
|
|
);
|
|
$stmt->execute([$fromId, $toId, $actionName]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'transition_id' => (int)db()->lastInsertId()
|
|
], 201);
|
|
}
|
|
|
|
case 'DeleteTransition': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse(['success' => false, 'error' => 'DeleteTransition requires POST.'], 405);
|
|
}
|
|
|
|
$transitionId = (int)($_GET['transition_id'] ?? 0);
|
|
|
|
if ($transitionId <= 0) {
|
|
jsonResponse(['success' => false, 'error' => 'Transition must be valid.'], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare('DELETE FROM task_state_transitions WHERE id = ?');
|
|
$stmt->execute([$transitionId]);
|
|
|
|
if ($stmt->rowCount() === 0) {
|
|
jsonResponse(['success' => false, 'error' => 'Transition not found.'], 404);
|
|
}
|
|
|
|
jsonResponse(['success' => true, 'transition_id' => $transitionId]);
|
|
}
|
|
|
|
case 'AssignState': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse(['success' => false, 'error' => 'AssignState requires POST.'], 405);
|
|
}
|
|
|
|
$data = getInputData();
|
|
$taskTypeId = (int)($data['task_type'] ?? 0);
|
|
$stateId = (int)($data['state'] ?? 0);
|
|
|
|
requireTaskType($taskTypeId);
|
|
requireState($stateId);
|
|
|
|
$existingStmt = db()->prepare(
|
|
'SELECT id
|
|
FROM assigned_task_states
|
|
WHERE task_type = ?
|
|
AND state = ?
|
|
LIMIT 1'
|
|
);
|
|
$existingStmt->execute([$taskTypeId, $stateId]);
|
|
$assignmentId = $existingStmt->fetchColumn();
|
|
|
|
if (!$assignmentId) {
|
|
$stmt = db()->prepare(
|
|
'INSERT INTO assigned_task_states (task_type, state)
|
|
VALUES (?, ?)'
|
|
);
|
|
$stmt->execute([$taskTypeId, $stateId]);
|
|
$assignmentId = db()->lastInsertId();
|
|
}
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'assignment_id' => (int)$assignmentId
|
|
], 201);
|
|
}
|
|
|
|
case 'SetDefaultState': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse(['success' => false, 'error' => 'SetDefaultState requires POST.'], 405);
|
|
}
|
|
|
|
$data = getInputData();
|
|
$taskTypeId = (int)($data['task_type'] ?? 0);
|
|
$stateId = isset($data['state']) && $data['state'] !== ''
|
|
? (int)$data['state']
|
|
: null;
|
|
|
|
requireTaskType($taskTypeId);
|
|
|
|
if ($stateId !== null) {
|
|
requireState($stateId);
|
|
|
|
$assignedStmt = db()->prepare(
|
|
'SELECT id
|
|
FROM assigned_task_states
|
|
WHERE task_type = ?
|
|
AND state = ?
|
|
LIMIT 1'
|
|
);
|
|
$assignedStmt->execute([$taskTypeId, $stateId]);
|
|
|
|
if (!$assignedStmt->fetch()) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Default state must be assigned to this task type.'
|
|
], 400);
|
|
}
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'UPDATE task_types
|
|
SET default_state = ?
|
|
WHERE id = ?'
|
|
);
|
|
$stmt->execute([$stateId, $taskTypeId]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'task_type' => $taskTypeId,
|
|
'default_state' => $stateId
|
|
]);
|
|
}
|
|
|
|
case 'RemoveAssignedState': {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jsonResponse(['success' => false, 'error' => 'RemoveAssignedState requires POST.'], 405);
|
|
}
|
|
|
|
$assignmentId = (int)($_GET['assignment_id'] ?? 0);
|
|
|
|
if ($assignmentId <= 0) {
|
|
jsonResponse(['success' => false, 'error' => 'Assignment must be valid.'], 400);
|
|
}
|
|
|
|
$stmt = db()->prepare('DELETE FROM assigned_task_states WHERE id = ?');
|
|
$lookupStmt = db()->prepare(
|
|
'SELECT task_type, state
|
|
FROM assigned_task_states
|
|
WHERE id = ?
|
|
LIMIT 1'
|
|
);
|
|
$lookupStmt->execute([$assignmentId]);
|
|
$assignment = $lookupStmt->fetch();
|
|
|
|
if (!$assignment) {
|
|
jsonResponse(['success' => false, 'error' => 'Assignment not found.'], 404);
|
|
}
|
|
|
|
$stmt->execute([$assignmentId]);
|
|
|
|
if ($stmt->rowCount() === 0) {
|
|
jsonResponse(['success' => false, 'error' => 'Assignment not found.'], 404);
|
|
}
|
|
|
|
db()->prepare(
|
|
'UPDATE task_types
|
|
SET default_state = NULL
|
|
WHERE id = ?
|
|
AND default_state = ?'
|
|
)->execute([
|
|
(int)$assignment['task_type'],
|
|
(int)$assignment['state']
|
|
]);
|
|
|
|
jsonResponse(['success' => true, 'assignment_id' => $assignmentId]);
|
|
}
|
|
|
|
default: {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'error' => 'Unknown API action.'
|
|
], 404);
|
|
}
|
|
}
|