359 lines
11 KiB
PHP
359 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
|
use PHPNative\Framework\Application;
|
|
use PHPNative\Ui\Widget\Button;
|
|
use PHPNative\Ui\Widget\Container;
|
|
use PHPNative\Ui\Widget\Label;
|
|
use PHPNative\Ui\Widget\TextInput;
|
|
use PHPNative\Ui\Window;
|
|
|
|
final class KanbanTaskCard extends Container
|
|
{
|
|
public function __construct(
|
|
private readonly string $boardId,
|
|
private readonly array $task,
|
|
private KanbanBoardView $boardView,
|
|
) {
|
|
parent::__construct('bg-white border border-gray-200 rounded shadow px-3 py-2 cursor-pointer');
|
|
|
|
$title = new Label($task['title'], 'text-sm text-black');
|
|
$this->addComponent($title);
|
|
if (!empty($task['note'] ?? '')) {
|
|
$this->addComponent(new Label($task['note'], 'text-xs text-gray-500'));
|
|
}
|
|
}
|
|
|
|
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
|
|
{
|
|
if ($button !== 1) {
|
|
return false;
|
|
}
|
|
|
|
$this->boardView->beginDrag($this->boardId, $this->task);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
final class KanbanBoardView extends Container
|
|
{
|
|
private array $boards;
|
|
private array $boardViews = [];
|
|
private null|array $dragState = null;
|
|
private null|string $hoverBoardId = null;
|
|
|
|
public function __construct(
|
|
private readonly string $storagePath,
|
|
array $initialBoards,
|
|
private readonly Label $statusLabel,
|
|
) {
|
|
parent::__construct('flex flex-row gap-4 flex-1');
|
|
$this->boards = $initialBoards;
|
|
|
|
$this->renderBoards();
|
|
}
|
|
|
|
public function addBoard(string $title): void
|
|
{
|
|
$title = trim($title);
|
|
if ($title === '') {
|
|
$this->statusLabel->setText('Board-Namen eingeben, um es hinzuzufügen.');
|
|
return;
|
|
}
|
|
|
|
$this->boards[] = [
|
|
'id' => uniqid('board_', true),
|
|
'title' => $title,
|
|
'tasks' => [],
|
|
];
|
|
|
|
$this->saveBoards();
|
|
$this->renderBoards();
|
|
$this->statusLabel->setText('Board hinzugefügt: ' . $title);
|
|
}
|
|
|
|
public function addTask(string $boardId, string $title): void
|
|
{
|
|
$title = trim($title);
|
|
if ($title === '') {
|
|
$this->statusLabel->setText('Bitte einen Aufgabentitel eingeben.');
|
|
return;
|
|
}
|
|
|
|
foreach ($this->boards as &$board) {
|
|
if ($board['id'] === $boardId) {
|
|
$board['tasks'][] = [
|
|
'id' => uniqid('task_', true),
|
|
'title' => $title,
|
|
'note' => '',
|
|
];
|
|
$this->saveBoards();
|
|
$this->renderBoards();
|
|
$this->statusLabel->setText(sprintf('Aufgabe "%s" zu "%s" hinzugefügt.', $title, $board['title']));
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->statusLabel->setText('Board nicht gefunden.');
|
|
}
|
|
|
|
public function beginDrag(string $boardId, array $task): void
|
|
{
|
|
$this->dragState = [
|
|
'fromBoardId' => $boardId,
|
|
'taskId' => $task['id'],
|
|
'taskData' => $task,
|
|
];
|
|
$this->statusLabel->setText('Verschiebe: ' . $task['title']);
|
|
}
|
|
|
|
public function handleMouseMove(float $mouseX, float $mouseY): void
|
|
{
|
|
parent::handleMouseMove($mouseX, $mouseY);
|
|
|
|
if ($this->dragState === null) {
|
|
return;
|
|
}
|
|
|
|
$target = $this->findBoardAt($mouseX, $mouseY);
|
|
if ($target !== $this->hoverBoardId) {
|
|
if ($this->hoverBoardId !== null) {
|
|
$this->setBoardHighlight($this->hoverBoardId, false);
|
|
}
|
|
if ($target !== null) {
|
|
$this->setBoardHighlight($target, true);
|
|
}
|
|
$this->hoverBoardId = $target;
|
|
}
|
|
}
|
|
|
|
public function handleMouseRelease(float $mouseX, float $mouseY, int $button): void
|
|
{
|
|
parent::handleMouseRelease($mouseX, $mouseY, $button);
|
|
|
|
if ($this->dragState === null || $button !== 1) {
|
|
return;
|
|
}
|
|
|
|
$dropTarget = $this->findBoardAt($mouseX, $mouseY);
|
|
$this->completeDrag($dropTarget);
|
|
}
|
|
|
|
private function renderBoards(): void
|
|
{
|
|
$this->clearChildren();
|
|
$this->boardViews = [];
|
|
|
|
foreach ($this->boards as $board) {
|
|
$style = 'flex flex-col w-[260] flex-none bg-white border border-black rounded-lg p-3 gap-2';
|
|
|
|
$column = new Container($style);
|
|
$column->addComponent(new Label('Board: ' . $board['title'], 'text-lg font-bold text-black'));
|
|
|
|
$taskTitles = array_map(static fn($t) => $t['title'], $board['tasks']);
|
|
$tasksText = empty($taskTitles) ? '(keine Aufgaben)' : implode(', ', $taskTitles);
|
|
$column->addComponent(new Label('Tasks: ' . $tasksText, 'text-xs text-black'));
|
|
|
|
if (!empty($board['tasks'])) {
|
|
$taskList = new Container('flex flex-col gap-2 mt-2');
|
|
foreach ($board['tasks'] as $task) {
|
|
$card = new Container('bg-white border-2 border-gray-400 rounded px-2 py-1 shadow-md');
|
|
$card->addComponent(new Label($task['title'], 'text-sm text-black'));
|
|
$taskList->addComponent($card);
|
|
}
|
|
$column->addComponent($taskList);
|
|
}
|
|
|
|
$this->addComponent($column);
|
|
$this->boardViews[$board['id']] = [
|
|
'column' => $column,
|
|
'defaultStyle' => $style,
|
|
'highlightStyle' => $style,
|
|
];
|
|
}
|
|
}
|
|
|
|
private function findBoardAt(float $mouseX, float $mouseY): null|string
|
|
{
|
|
foreach ($this->boardViews as $boardId => $info) {
|
|
$viewport = $info['column']->getViewport();
|
|
if (
|
|
$mouseX >= $viewport->x &&
|
|
$mouseX <= ($viewport->x + $viewport->width) &&
|
|
$mouseY >= $viewport->y &&
|
|
$mouseY <= ($viewport->y + $viewport->height)
|
|
) {
|
|
return $boardId;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function setBoardHighlight(string $boardId, bool $active): void
|
|
{
|
|
if (!isset($this->boardViews[$boardId])) {
|
|
return;
|
|
}
|
|
|
|
$column = $this->boardViews[$boardId]['column'];
|
|
$style = $active ? $this->boardViews[$boardId]['highlightStyle'] : $this->boardViews[$boardId]['defaultStyle'];
|
|
$column->setStyle($style);
|
|
}
|
|
|
|
private function completeDrag(null|string $targetBoardId): void
|
|
{
|
|
$state = $this->dragState;
|
|
$this->dragState = null;
|
|
|
|
if ($this->hoverBoardId !== null) {
|
|
$this->setBoardHighlight($this->hoverBoardId, false);
|
|
$this->hoverBoardId = null;
|
|
}
|
|
|
|
if (!$state) {
|
|
return;
|
|
}
|
|
|
|
if ($targetBoardId === null) {
|
|
$this->statusLabel->setText('Ziehen abgebrochen.');
|
|
return;
|
|
}
|
|
|
|
if ($targetBoardId === $state['fromBoardId']) {
|
|
$this->statusLabel->setText('Aufgabe blieb im gleichen Board.');
|
|
return;
|
|
}
|
|
|
|
$movedTask = null;
|
|
foreach ($this->boards as &$board) {
|
|
if ($board['id'] === $state['fromBoardId']) {
|
|
foreach ($board['tasks'] as $index => $task) {
|
|
if ($task['id'] === $state['taskId']) {
|
|
$movedTask = $task;
|
|
array_splice($board['tasks'], $index, 1);
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($movedTask === null) {
|
|
$this->statusLabel->setText('Aufgabe konnte nicht verschoben werden.');
|
|
return;
|
|
}
|
|
|
|
foreach ($this->boards as &$board) {
|
|
if ($board['id'] === $targetBoardId) {
|
|
$board['tasks'][] = $movedTask;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->saveBoards();
|
|
$this->renderBoards();
|
|
|
|
$this->statusLabel->setText(sprintf(
|
|
'"%s" nach "%s" verschoben.',
|
|
$movedTask['title'],
|
|
$this->getBoardTitle($targetBoardId),
|
|
));
|
|
}
|
|
|
|
private function getBoardTitle(string $boardId): string
|
|
{
|
|
foreach ($this->boards as $board) {
|
|
if ($board['id'] === $boardId) {
|
|
return $board['title'];
|
|
}
|
|
}
|
|
|
|
return $boardId;
|
|
}
|
|
|
|
private function saveBoards(): void
|
|
{
|
|
file_put_contents(
|
|
$this->storagePath,
|
|
json_encode($this->boards, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
|
|
LOCK_EX,
|
|
);
|
|
}
|
|
}
|
|
|
|
$storagePath = __DIR__ . '/kanban_data.json';
|
|
|
|
$boards = [];
|
|
if (is_file($storagePath)) {
|
|
$raw = file_get_contents($storagePath);
|
|
$decoded = json_decode($raw ?: '[]', true);
|
|
if (is_array($decoded)) {
|
|
$boards = $decoded;
|
|
}
|
|
}
|
|
|
|
if (empty($boards)) {
|
|
$boards = [
|
|
[
|
|
'id' => uniqid('board_', true),
|
|
'title' => 'Backlog',
|
|
'tasks' => [
|
|
['id' => uniqid('task_', true), 'title' => 'Idee sammeln', 'note' => ''],
|
|
['id' => uniqid('task_', true), 'title' => 'Mockups skizzieren', 'note' => ''],
|
|
],
|
|
],
|
|
[
|
|
'id' => uniqid('board_', true),
|
|
'title' => 'In Arbeit',
|
|
'tasks' => [
|
|
['id' => uniqid('task_', true), 'title' => 'API anbinden', 'note' => ''],
|
|
],
|
|
],
|
|
[
|
|
'id' => uniqid('board_', true),
|
|
'title' => 'Erledigt',
|
|
'tasks' => [
|
|
['id' => uniqid('task_', true), 'title' => 'UI Grundlayout', 'note' => ''],
|
|
],
|
|
],
|
|
];
|
|
file_put_contents($storagePath, json_encode($boards, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX);
|
|
}
|
|
|
|
$app = new Application();
|
|
$window = new Window('Kanban Beispiel', 1200, 800);
|
|
|
|
$root = new Container('flex flex-col h-full w-full bg-gray-100 gap-4 p-4');
|
|
|
|
$title = new Label('Kanban Board', 'text-3xl font-bold text-black');
|
|
$subTitle = new Label(
|
|
'Boards hinzufügen, Aufgaben erstellen und per Drag & Drop verschieben.',
|
|
'text-base text-gray-700',
|
|
);
|
|
$root->addComponent($title);
|
|
$root->addComponent($subTitle);
|
|
|
|
$statusLabel = new Label('Bereit.', 'text-sm text-gray-600');
|
|
$kanbanView = new KanbanBoardView($storagePath, $boards, $statusLabel);
|
|
|
|
$boardInput = new TextInput('Neues Board', 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black');
|
|
$boardAddButton = new Button('Board hinzufügen', 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700');
|
|
$boardAddButton->setOnClick(function () use ($kanbanView, $boardInput, $statusLabel): void {
|
|
$kanbanView->addBoard($boardInput->getValue());
|
|
$boardInput->setValue('');
|
|
});
|
|
$boardRow = new Container('flex flex-row gap-3 items-center');
|
|
$boardRow->addComponent($boardInput);
|
|
$boardRow->addComponent($boardAddButton);
|
|
$root->addComponent($boardRow);
|
|
|
|
$root->addComponent($kanbanView);
|
|
$root->addComponent($statusLabel);
|
|
|
|
$window->setRoot($root);
|
|
$app->addWindow($window);
|
|
$app->run();
|