397 lines
14 KiB
PHP
397 lines
14 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,
|
|
bool $selected = false,
|
|
) {
|
|
$baseStyle = 'bg-white border rounded px-3 py-2 shadow cursor-pointer';
|
|
$style = $selected ? ($baseStyle . ' border-blue-500 bg-blue-50') : ($baseStyle . ' border-gray-200');
|
|
|
|
parent::__construct($style);
|
|
|
|
$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->selectTask($this->boardId, $this->task);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
final class KanbanBoardHeader extends Container
|
|
{
|
|
public function __construct(
|
|
private readonly string $boardId,
|
|
private KanbanBoardView $boardView,
|
|
string $title,
|
|
int $taskCount,
|
|
) {
|
|
parent::__construct('flex flex-col gap-1 cursor-pointer');
|
|
|
|
$this->addComponent(new Label($title, 'text-lg font-bold text-black'));
|
|
$this->addComponent(new Label(sprintf('%d Aufgaben', $taskCount), 'text-xs text-gray-700'));
|
|
$this->addComponent(new Label('Hierhin verschieben (Klick)', 'text-[10] text-gray-500'));
|
|
}
|
|
|
|
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
|
|
{
|
|
if ($button !== 1) {
|
|
return false;
|
|
}
|
|
|
|
// Nur reagieren, wenn der Klick innerhalb des Header-Viewports liegt
|
|
$vp = $this->getViewport();
|
|
$inside =
|
|
$mouseX >= $vp->x &&
|
|
$mouseX <= ($vp->x + $vp->width) &&
|
|
$mouseY >= $vp->y &&
|
|
$mouseY <= ($vp->y + $vp->height);
|
|
|
|
if (!$inside) {
|
|
return false;
|
|
}
|
|
|
|
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
|
error_log(sprintf('[KanbanHeader] Click on board "%s"', $this->boardId));
|
|
}
|
|
|
|
$this->boardView->moveSelectedToBoard($this->boardId);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
final class KanbanBoardView extends Container
|
|
{
|
|
private array $boards;
|
|
private array $boardViews = [];
|
|
private null|string $selectedTaskId = null;
|
|
private null|string $selectedBoardId = 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 selectTask(string $boardId, array $task): void
|
|
{
|
|
$this->selectedBoardId = $boardId;
|
|
$this->selectedTaskId = $task['id'];
|
|
$msg = sprintf('Ausgewählt: "%s" in "%s"', $task['title'], $this->getBoardTitle($boardId));
|
|
$this->statusLabel->setText($msg);
|
|
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
|
error_log('[Kanban] ' . $msg);
|
|
}
|
|
$this->renderBoards();
|
|
}
|
|
|
|
public function moveSelectedToBoard(string $targetBoardId): void
|
|
{
|
|
if ($this->selectedTaskId === null) {
|
|
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
|
error_log('[Kanban] moveSelectedToBoard called without selectedTaskId');
|
|
}
|
|
$this->statusLabel->setText('Keine Aufgabe ausgewählt.');
|
|
return;
|
|
}
|
|
|
|
if ($this->selectedBoardId === $targetBoardId) {
|
|
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
|
error_log('[Kanban] moveSelectedToBoard target is same board');
|
|
}
|
|
$this->statusLabel->setText('Aufgabe ist bereits in diesem Board.');
|
|
return;
|
|
}
|
|
|
|
$movedTask = null;
|
|
foreach ($this->boards as &$board) {
|
|
if ($board['id'] === $this->selectedBoardId) {
|
|
foreach ($board['tasks'] as $index => $task) {
|
|
if ($task['id'] === $this->selectedTaskId) {
|
|
$movedTask = $task;
|
|
array_splice($board['tasks'], $index, 1);
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($movedTask === null) {
|
|
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
|
error_log('[Kanban] moveSelectedToBoard: task not found in source board');
|
|
}
|
|
$this->statusLabel->setText('Ausgewählte Aufgabe konnte nicht gefunden werden.');
|
|
return;
|
|
}
|
|
|
|
foreach ($this->boards as &$board) {
|
|
if ($board['id'] === $targetBoardId) {
|
|
$board['tasks'][] = $movedTask;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Nach erfolgreichem Verschieben Auswahl zurücksetzen,
|
|
// damit weitere Board-Klicks den Task nicht erneut verschieben.
|
|
$this->selectedBoardId = null;
|
|
$this->selectedTaskId = null;
|
|
|
|
$this->saveBoards();
|
|
$this->renderBoards();
|
|
$msg = sprintf('"%s" nach "%s" verschoben.', $movedTask['title'], $this->getBoardTitle($targetBoardId));
|
|
$this->statusLabel->setText($msg);
|
|
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
|
error_log('[Kanban] ' . $msg);
|
|
}
|
|
}
|
|
|
|
private function renderBoards(): void
|
|
{
|
|
$this->clearChildren();
|
|
$this->boardViews = [];
|
|
|
|
foreach ($this->boards as $board) {
|
|
$defaultStyle = 'flex flex-col w-[340] flex-none bg-white border border-gray-300 rounded-2xl p-5 gap-3';
|
|
$highlightStyle = 'flex flex-col w-[340] flex-none bg-blue-50 border-2 border-blue-500 rounded-2xl p-5 gap-3';
|
|
|
|
$column = new Container($defaultStyle);
|
|
|
|
// Header mit Titel und Task-Anzahl, klickbar als Drop-Ziel
|
|
$header = new KanbanBoardHeader($board['id'], $this, $board['title'], count($board['tasks']));
|
|
$column->addComponent($header);
|
|
|
|
// Task-Karten (Buttons) mit Auswahl-Hervorhebung
|
|
if (!empty($board['tasks'])) {
|
|
$taskList = new Container('flex flex-col gap-3 mt-3');
|
|
foreach ($board['tasks'] as $task) {
|
|
$isSelected = $this->selectedTaskId === $task['id'];
|
|
$style = $isSelected
|
|
? 'w-full text-left bg-blue-50 border border-blue-500 rounded-lg px-4 py-3 shadow-md cursor-pointer'
|
|
: 'w-full text-left bg-white border border-gray-200 rounded-lg px-4 py-3 shadow-md cursor-pointer';
|
|
|
|
$button = new Button($task['title'], $style);
|
|
$button->setOnClick(function () use ($board, $task): void {
|
|
// Debug-Ausgabe in Statuszeile, um Klicks sicher zu sehen
|
|
$this->statusLabel->setText(sprintf(
|
|
'Task-Klick: "%s" in "%s"',
|
|
$task['title'],
|
|
$this->getBoardTitle($board['id']),
|
|
));
|
|
$this->selectTask($board['id'], $task);
|
|
});
|
|
|
|
$taskList->addComponent($button);
|
|
}
|
|
$column->addComponent($taskList);
|
|
}
|
|
|
|
$this->addComponent($column);
|
|
$this->boardViews[$board['id']] = [
|
|
'column' => $column,
|
|
'defaultStyle' => $defaultStyle,
|
|
'highlightStyle' => $highlightStyle,
|
|
];
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
define('DEBUG_EVENTS', false);
|
|
$app = new Application();
|
|
$window = new Window('Kanban Beispiel', 1200, 800);
|
|
|
|
$root = new Container('flex flex-col h-full w-full bg-gray-100 gap-6 p-6');
|
|
|
|
$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);
|
|
|
|
// SDL_Window PixelDensity und DisplayScale im Status anzeigen (falls verfügbar)
|
|
$densityText = '';
|
|
if (function_exists('sdl_get_window_pixel_density')) {
|
|
$density = sdl_get_window_pixel_density($window->getWindowResource());
|
|
$densityText .= sprintf('PixelDensity: %.2f', $density);
|
|
}
|
|
if (function_exists('sdl_get_window_display_scale')) {
|
|
$scale = sdl_get_window_display_scale($window->getWindowResource());
|
|
$densityText .= ($densityText !== '' ? ' | ' : '') . sprintf('DisplayScale: %.2f', $scale);
|
|
}
|
|
if (function_exists('sdl_get_display_content_scale')) {
|
|
$contentScale = sdl_get_display_content_scale($window->getWindowResource());
|
|
$densityText .= ($densityText !== '' ? ' | ' : '') . sprintf('ContentScale: %.2f', $contentScale);
|
|
}
|
|
// Fallback: effektiven Scale aus WindowSize vs. WindowSizeInPixels berechnen
|
|
if (function_exists('sdl_get_window_size') && function_exists('sdl_get_window_size_in_pixels')) {
|
|
$logical = sdl_get_window_size($window->getWindowResource());
|
|
$pixels = sdl_get_window_size_in_pixels($window->getWindowResource());
|
|
if (is_array($logical) && is_array($pixels) && $logical[0] > 0 && $logical[1] > 0) {
|
|
$scaleX = $pixels[0] / $logical[0];
|
|
$scaleY = $pixels[1] / $logical[1];
|
|
$densityText .= ($densityText !== '' ? ' | ' : '') . sprintf('EffectiveScale: %.2f x %.2f', $scaleX, $scaleY);
|
|
}
|
|
}
|
|
if (function_exists('sdl_get_current_video_driver')) {
|
|
$driver = sdl_get_current_video_driver();
|
|
if ($driver !== false) {
|
|
$densityText .= ($densityText !== '' ? ' | ' : '') . 'VideoDriver: ' . $driver;
|
|
}
|
|
}
|
|
if ($densityText !== '') {
|
|
$statusLabel->setText('Bereit. ' . $densityText);
|
|
}
|
|
|
|
$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();
|