Files
Projectkiln/ProjectKiln/api/workflow.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);
}
}