| @@ -173,35 +173,6 @@ class BoardsController extends Controller | |||||
| return $this->redirect('/board/' . $board->slug); | return $this->redirect('/board/' . $board->slug); | ||||
| } | } | ||||
| public function updateCardAgeSettings(Request $request, int $id): mixed | |||||
| { | |||||
| if (!AuthService::isLoggedIn()) { | |||||
| return $this->json(['ok' => false, 'error' => 'Unauthorized'], 401); | |||||
| } | |||||
| $row = $this->boards()->find($id); | |||||
| if ($row === null) { | |||||
| return $this->json(['ok' => false, 'error' => 'Not found'], 404); | |||||
| } | |||||
| $showCardAge = (string) $request->input('show_card_age', '0') === '1'; | |||||
| $cardAgeWarningDays = max(0, (int) $request->input('card_age_warning_days', 0)); | |||||
| $this->boards()->updateCardAgeSettings( | |||||
| $id, | |||||
| $showCardAge, | |||||
| $cardAgeWarningDays, | |||||
| date('Y-m-d H:i:s'), | |||||
| AuthService::getCurrentUsername() | |||||
| ); | |||||
| return $this->json([ | |||||
| 'ok' => true, | |||||
| 'show_card_age' => $showCardAge, | |||||
| 'card_age_warning_days' => $cardAgeWarningDays, | |||||
| ]); | |||||
| } | |||||
| public function destroy(string $slug): mixed | public function destroy(string $slug): mixed | ||||
| { | { | ||||
| if ($guard = AuthService::requireLogin()) { | if ($guard = AuthService::requireLogin()) { | ||||
| @@ -97,6 +97,35 @@ class ColumnsController extends Controller | |||||
| return $this->json(['ok' => true, 'show_card_count' => $show]); | return $this->json(['ok' => true, 'show_card_count' => $show]); | ||||
| } | } | ||||
| public function updateCardAgeSettings(Request $request, int $id): mixed | |||||
| { | |||||
| if (!AuthService::isLoggedIn()) { | |||||
| return $this->json(['ok' => false, 'error' => 'Unauthorized'], 401); | |||||
| } | |||||
| $row = $this->columns()->find($id); | |||||
| if ($row === null) { | |||||
| return $this->json(['ok' => false, 'error' => 'Not found'], 404); | |||||
| } | |||||
| $showCardAge = (string) $request->input('show_card_age', '0') === '1'; | |||||
| $cardAgeWarningDays = max(0, (int) $request->input('card_age_warning_days', 0)); | |||||
| $this->columns()->updateCardAgeSettings( | |||||
| $id, | |||||
| $showCardAge, | |||||
| $cardAgeWarningDays, | |||||
| date('Y-m-d H:i:s'), | |||||
| AuthService::getCurrentUsername() | |||||
| ); | |||||
| return $this->json([ | |||||
| 'ok' => true, | |||||
| 'show_card_age' => $showCardAge, | |||||
| 'card_age_warning_days' => $cardAgeWarningDays, | |||||
| ]); | |||||
| } | |||||
| public function destroy(int $id): mixed | public function destroy(int $id): mixed | ||||
| { | { | ||||
| if (!AuthService::isLoggedIn()) { | if (!AuthService::isLoggedIn()) { | ||||
| @@ -79,6 +79,35 @@ class SwimLanesController extends Controller | |||||
| return $this->json(['ok' => true]); | return $this->json(['ok' => true]); | ||||
| } | } | ||||
| public function updateCardAgeSettings(Request $request, int $id): mixed | |||||
| { | |||||
| if (!AuthService::isLoggedIn()) { | |||||
| return $this->json(['ok' => false, 'error' => 'Unauthorized'], 401); | |||||
| } | |||||
| $row = $this->lanes()->find($id); | |||||
| if ($row === null) { | |||||
| return $this->json(['ok' => false, 'error' => 'Not found'], 404); | |||||
| } | |||||
| $showCardAge = (string) $request->input('show_card_age', '0') === '1'; | |||||
| $cardAgeWarningDays = max(0, (int) $request->input('card_age_warning_days', 0)); | |||||
| $this->lanes()->updateCardAgeSettings( | |||||
| $id, | |||||
| $showCardAge, | |||||
| $cardAgeWarningDays, | |||||
| date('Y-m-d H:i:s'), | |||||
| AuthService::getCurrentUsername() | |||||
| ); | |||||
| return $this->json([ | |||||
| 'ok' => true, | |||||
| 'show_card_age' => $showCardAge, | |||||
| 'card_age_warning_days' => $cardAgeWarningDays, | |||||
| ]); | |||||
| } | |||||
| public function destroy(int $id): mixed | public function destroy(int $id): mixed | ||||
| { | { | ||||
| if (!AuthService::isLoggedIn()) { | if (!AuthService::isLoggedIn()) { | ||||
| @@ -11,8 +11,6 @@ class Board | |||||
| public string $slug = ''; | public string $slug = ''; | ||||
| public bool $importFromPrintstream = false; | public bool $importFromPrintstream = false; | ||||
| public string $printstreamJobName = ''; | public string $printstreamJobName = ''; | ||||
| public bool $showCardAge = false; | |||||
| public int $cardAgeWarningDays = 0; | |||||
| public ?string $createdAt = null; | public ?string $createdAt = null; | ||||
| public string $createdBy = ''; | public string $createdBy = ''; | ||||
| public ?string $updatedAt = null; | public ?string $updatedAt = null; | ||||
| @@ -26,8 +24,6 @@ class Board | |||||
| $model->slug = (string) ($row['slug'] ?? ''); | $model->slug = (string) ($row['slug'] ?? ''); | ||||
| $model->importFromPrintstream = (bool) ($row['import_from_printstream'] ?? false); | $model->importFromPrintstream = (bool) ($row['import_from_printstream'] ?? false); | ||||
| $model->printstreamJobName = (string) ($row['printstream_job_name'] ?? ''); | $model->printstreamJobName = (string) ($row['printstream_job_name'] ?? ''); | ||||
| $model->showCardAge = (bool) ($row['show_card_age'] ?? false); | |||||
| $model->cardAgeWarningDays = (int) ($row['card_age_warning_days'] ?? 0); | |||||
| $model->createdAt = $row['created_at'] ?? null; | $model->createdAt = $row['created_at'] ?? null; | ||||
| $model->createdBy = (string) ($row['created_by'] ?? ''); | $model->createdBy = (string) ($row['created_by'] ?? ''); | ||||
| $model->updatedAt = $row['updated_at'] ?? null; | $model->updatedAt = $row['updated_at'] ?? null; | ||||
| @@ -11,6 +11,8 @@ class BoardColumn | |||||
| public string $name = ''; | public string $name = ''; | ||||
| public int $position = 0; | public int $position = 0; | ||||
| public bool $showCardCount = false; | public bool $showCardCount = false; | ||||
| public bool $showCardAge = false; | |||||
| public int $cardAgeWarningDays = 0; | |||||
| public ?string $createdAt = null; | public ?string $createdAt = null; | ||||
| public string $createdBy = ''; | public string $createdBy = ''; | ||||
| public ?string $updatedAt = null; | public ?string $updatedAt = null; | ||||
| @@ -19,15 +21,17 @@ class BoardColumn | |||||
| public static function fromRow(array $row): self | public static function fromRow(array $row): self | ||||
| { | { | ||||
| $model = new self(); | $model = new self(); | ||||
| $model->id = (int) ($row['id'] ?? 0); | |||||
| $model->boardId = (int) ($row['board_id'] ?? 0); | |||||
| $model->name = (string) ($row['name'] ?? ''); | |||||
| $model->position = (int) ($row['position'] ?? 0); | |||||
| $model->showCardCount = (bool) ($row['show_card_count'] ?? false); | |||||
| $model->createdAt = $row['created_at'] ?? null; | |||||
| $model->createdBy = (string) ($row['created_by'] ?? ''); | |||||
| $model->updatedAt = $row['updated_at'] ?? null; | |||||
| $model->updatedBy = (string) ($row['updated_by'] ?? ''); | |||||
| $model->id = (int) ($row['id'] ?? 0); | |||||
| $model->boardId = (int) ($row['board_id'] ?? 0); | |||||
| $model->name = (string) ($row['name'] ?? ''); | |||||
| $model->position = (int) ($row['position'] ?? 0); | |||||
| $model->showCardCount = (bool) ($row['show_card_count'] ?? false); | |||||
| $model->showCardAge = (bool) ($row['show_card_age'] ?? false); | |||||
| $model->cardAgeWarningDays = (int) ($row['card_age_warning_days'] ?? 0); | |||||
| $model->createdAt = $row['created_at'] ?? null; | |||||
| $model->createdBy = (string) ($row['created_by'] ?? ''); | |||||
| $model->updatedAt = $row['updated_at'] ?? null; | |||||
| $model->updatedBy = (string) ($row['updated_by'] ?? ''); | |||||
| return $model; | return $model; | ||||
| } | } | ||||
| @@ -10,6 +10,8 @@ class SwimLane | |||||
| public int $boardId = 0; | public int $boardId = 0; | ||||
| public string $name = ''; | public string $name = ''; | ||||
| public int $position = 0; | public int $position = 0; | ||||
| public bool $showCardAge = false; | |||||
| public int $cardAgeWarningDays = 0; | |||||
| public ?string $createdAt = null; | public ?string $createdAt = null; | ||||
| public string $createdBy = ''; | public string $createdBy = ''; | ||||
| public ?string $updatedAt = null; | public ?string $updatedAt = null; | ||||
| @@ -18,14 +20,16 @@ class SwimLane | |||||
| public static function fromRow(array $row): self | public static function fromRow(array $row): self | ||||
| { | { | ||||
| $model = new self(); | $model = new self(); | ||||
| $model->id = (int) ($row['id'] ?? 0); | |||||
| $model->boardId = (int) ($row['board_id'] ?? 0); | |||||
| $model->name = (string) ($row['name'] ?? ''); | |||||
| $model->position = (int) ($row['position'] ?? 0); | |||||
| $model->createdAt = $row['created_at'] ?? null; | |||||
| $model->createdBy = (string) ($row['created_by'] ?? ''); | |||||
| $model->updatedAt = $row['updated_at'] ?? null; | |||||
| $model->updatedBy = (string) ($row['updated_by'] ?? ''); | |||||
| $model->id = (int) ($row['id'] ?? 0); | |||||
| $model->boardId = (int) ($row['board_id'] ?? 0); | |||||
| $model->name = (string) ($row['name'] ?? ''); | |||||
| $model->position = (int) ($row['position'] ?? 0); | |||||
| $model->showCardAge = (bool) ($row['show_card_age'] ?? false); | |||||
| $model->cardAgeWarningDays = (int) ($row['card_age_warning_days'] ?? 0); | |||||
| $model->createdAt = $row['created_at'] ?? null; | |||||
| $model->createdBy = (string) ($row['created_by'] ?? ''); | |||||
| $model->updatedAt = $row['updated_at'] ?? null; | |||||
| $model->updatedBy = (string) ($row['updated_by'] ?? ''); | |||||
| return $model; | return $model; | ||||
| } | } | ||||
| @@ -35,17 +35,19 @@ class BoardColumnRepository extends Repository | |||||
| public function insert(BoardColumn $col): BoardColumn | public function insert(BoardColumn $col): BoardColumn | ||||
| { | { | ||||
| $this->database->execute( | $this->database->execute( | ||||
| 'INSERT INTO board_columns (board_id, name, position, show_card_count, created_at, created_by, updated_at, updated_by) | |||||
| VALUES (:board_id, :name, :position, :show_card_count, :created_at, :created_by, :updated_at, :updated_by)', | |||||
| 'INSERT INTO board_columns (board_id, name, position, show_card_count, show_card_age, card_age_warning_days, created_at, created_by, updated_at, updated_by) | |||||
| VALUES (:board_id, :name, :position, :show_card_count, :show_card_age, :card_age_warning_days, :created_at, :created_by, :updated_at, :updated_by)', | |||||
| [ | [ | ||||
| 'board_id' => $col->boardId, | |||||
| 'name' => $col->name, | |||||
| 'position' => $col->position, | |||||
| 'show_card_count' => $col->showCardCount ? 1 : 0, | |||||
| 'created_at' => $col->createdAt, | |||||
| 'created_by' => $col->createdBy, | |||||
| 'updated_at' => $col->updatedAt, | |||||
| 'updated_by' => $col->updatedBy, | |||||
| 'board_id' => $col->boardId, | |||||
| 'name' => $col->name, | |||||
| 'position' => $col->position, | |||||
| 'show_card_count' => $col->showCardCount ? 1 : 0, | |||||
| 'show_card_age' => $col->showCardAge ? 1 : 0, | |||||
| 'card_age_warning_days' => $col->cardAgeWarningDays, | |||||
| 'created_at' => $col->createdAt, | |||||
| 'created_by' => $col->createdBy, | |||||
| 'updated_at' => $col->updatedAt, | |||||
| 'updated_by' => $col->updatedBy, | |||||
| ] | ] | ||||
| ); | ); | ||||
| @@ -71,6 +73,21 @@ class BoardColumnRepository extends Repository | |||||
| ); | ); | ||||
| } | } | ||||
| public function updateCardAgeSettings(int $id, bool $showCardAge, int $cardAgeWarningDays, string $updatedAt, string $updatedBy): void | |||||
| { | |||||
| $this->database->execute( | |||||
| 'UPDATE board_columns SET show_card_age = :show_card_age, card_age_warning_days = :card_age_warning_days, | |||||
| updated_at = :updated_at, updated_by = :updated_by WHERE id = :id', | |||||
| [ | |||||
| 'show_card_age' => $showCardAge ? 1 : 0, | |||||
| 'card_age_warning_days' => $cardAgeWarningDays, | |||||
| 'updated_at' => $updatedAt, | |||||
| 'updated_by' => $updatedBy, | |||||
| 'id' => $id, | |||||
| ] | |||||
| ); | |||||
| } | |||||
| public function updatePosition(int $id, int $position, string $updatedAt, string $updatedBy): void | public function updatePosition(int $id, int $position, string $updatedAt, string $updatedBy): void | ||||
| { | { | ||||
| $this->database->execute( | $this->database->execute( | ||||
| @@ -63,18 +63,14 @@ class BoardRepository extends Repository | |||||
| { | { | ||||
| $this->database->execute( | $this->database->execute( | ||||
| 'INSERT INTO boards | 'INSERT INTO boards | ||||
| (name, slug, import_from_printstream, printstream_job_name, show_card_age, card_age_warning_days, | |||||
| created_at, created_by, updated_at, updated_by) | |||||
| (name, slug, import_from_printstream, printstream_job_name, created_at, created_by, updated_at, updated_by) | |||||
| VALUES | VALUES | ||||
| (:name, :slug, :import_from_printstream, :printstream_job_name, :show_card_age, :card_age_warning_days, | |||||
| :created_at, :created_by, :updated_at, :updated_by)', | |||||
| (:name, :slug, :import_from_printstream, :printstream_job_name, :created_at, :created_by, :updated_at, :updated_by)', | |||||
| [ | [ | ||||
| 'name' => $board->name, | 'name' => $board->name, | ||||
| 'slug' => $board->slug, | 'slug' => $board->slug, | ||||
| 'import_from_printstream' => $board->importFromPrintstream ? 1 : 0, | 'import_from_printstream' => $board->importFromPrintstream ? 1 : 0, | ||||
| 'printstream_job_name' => $board->printstreamJobName, | 'printstream_job_name' => $board->printstreamJobName, | ||||
| 'show_card_age' => $board->showCardAge ? 1 : 0, | |||||
| 'card_age_warning_days' => $board->cardAgeWarningDays, | |||||
| 'created_at' => $board->createdAt, | 'created_at' => $board->createdAt, | ||||
| 'created_by' => $board->createdBy, | 'created_by' => $board->createdBy, | ||||
| 'updated_at' => $board->updatedAt, | 'updated_at' => $board->updatedAt, | ||||
| @@ -106,21 +102,4 @@ class BoardRepository extends Repository | |||||
| ] | ] | ||||
| ); | ); | ||||
| } | } | ||||
| public function updateCardAgeSettings(int $id, bool $showCardAge, int $cardAgeWarningDays, string $updatedAt, string $updatedBy): void | |||||
| { | |||||
| $this->database->execute( | |||||
| 'UPDATE boards | |||||
| SET show_card_age = :show_card_age, card_age_warning_days = :card_age_warning_days, | |||||
| updated_at = :updated_at, updated_by = :updated_by | |||||
| WHERE id = :id', | |||||
| [ | |||||
| 'show_card_age' => $showCardAge ? 1 : 0, | |||||
| 'card_age_warning_days' => $cardAgeWarningDays, | |||||
| 'updated_at' => $updatedAt, | |||||
| 'updated_by' => $updatedBy, | |||||
| 'id' => $id, | |||||
| ] | |||||
| ); | |||||
| } | |||||
| } | } | ||||
| @@ -35,16 +35,18 @@ class SwimLaneRepository extends Repository | |||||
| public function insert(SwimLane $lane): SwimLane | public function insert(SwimLane $lane): SwimLane | ||||
| { | { | ||||
| $this->database->execute( | $this->database->execute( | ||||
| 'INSERT INTO swim_lanes (board_id, name, position, created_at, created_by, updated_at, updated_by) | |||||
| VALUES (:board_id, :name, :position, :created_at, :created_by, :updated_at, :updated_by)', | |||||
| 'INSERT INTO swim_lanes (board_id, name, position, show_card_age, card_age_warning_days, created_at, created_by, updated_at, updated_by) | |||||
| VALUES (:board_id, :name, :position, :show_card_age, :card_age_warning_days, :created_at, :created_by, :updated_at, :updated_by)', | |||||
| [ | [ | ||||
| 'board_id' => $lane->boardId, | |||||
| 'name' => $lane->name, | |||||
| 'position' => $lane->position, | |||||
| 'created_at' => $lane->createdAt, | |||||
| 'created_by' => $lane->createdBy, | |||||
| 'updated_at' => $lane->updatedAt, | |||||
| 'updated_by' => $lane->updatedBy, | |||||
| 'board_id' => $lane->boardId, | |||||
| 'name' => $lane->name, | |||||
| 'position' => $lane->position, | |||||
| 'show_card_age' => $lane->showCardAge ? 1 : 0, | |||||
| 'card_age_warning_days' => $lane->cardAgeWarningDays, | |||||
| 'created_at' => $lane->createdAt, | |||||
| 'created_by' => $lane->createdBy, | |||||
| 'updated_at' => $lane->updatedAt, | |||||
| 'updated_by' => $lane->updatedBy, | |||||
| ] | ] | ||||
| ); | ); | ||||
| @@ -62,6 +64,21 @@ class SwimLaneRepository extends Repository | |||||
| ); | ); | ||||
| } | } | ||||
| public function updateCardAgeSettings(int $id, bool $showCardAge, int $cardAgeWarningDays, string $updatedAt, string $updatedBy): void | |||||
| { | |||||
| $this->database->execute( | |||||
| 'UPDATE swim_lanes SET show_card_age = :show_card_age, card_age_warning_days = :card_age_warning_days, | |||||
| updated_at = :updated_at, updated_by = :updated_by WHERE id = :id', | |||||
| [ | |||||
| 'show_card_age' => $showCardAge ? 1 : 0, | |||||
| 'card_age_warning_days' => $cardAgeWarningDays, | |||||
| 'updated_at' => $updatedAt, | |||||
| 'updated_by' => $updatedBy, | |||||
| 'id' => $id, | |||||
| ] | |||||
| ); | |||||
| } | |||||
| public function updatePosition(int $id, int $position, string $updatedAt, string $updatedBy): void | public function updatePosition(int $id, int $position, string $updatedAt, string $updatedBy): void | ||||
| { | { | ||||
| $this->database->execute( | $this->database->execute( | ||||
| @@ -84,12 +84,10 @@ | |||||
| var KANBAN = { | var KANBAN = { | ||||
| boardId: <?= (int) $board->id ?>, | boardId: <?= (int) $board->id ?>, | ||||
| boardSlug: "<?= e($board->slug) ?>", | boardSlug: "<?= e($board->slug) ?>", | ||||
| cards: <?= $cardsJson ?>, | |||||
| showCardAge: <?= $board->showCardAge ? 'true' : 'false' ?>, | |||||
| cardAgeWarningDays: <?= (int) $board->cardAgeWarningDays ?> | |||||
| cards: <?= $cardsJson ?> | |||||
| }; | }; | ||||
| var KANBAN_COLS = <?= json_encode(array_map(fn($c) => ['id' => $c->id, 'name' => $c->name, 'position' => $c->position, 'show_card_count' => $c->showCardCount], $columns)) ?>; | |||||
| var KANBAN_LANES = <?= json_encode(array_map(fn($l) => ['id' => $l->id, 'name' => $l->name, 'position' => $l->position], $lanes)) ?>; | |||||
| var KANBAN_COLS = <?= json_encode(array_map(fn($c) => ['id' => $c->id, 'name' => $c->name, 'position' => $c->position, 'show_card_count' => $c->showCardCount, 'show_card_age' => $c->showCardAge, 'card_age_warning_days' => $c->cardAgeWarningDays], $columns)) ?>; | |||||
| var KANBAN_LANES = <?= json_encode(array_map(fn($l) => ['id' => $l->id, 'name' => $l->name, 'position' => $l->position, 'show_card_age' => $l->showCardAge, 'card_age_warning_days' => $l->cardAgeWarningDays], $lanes)) ?>; | |||||
| </script> | </script> | ||||
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> | <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> | ||||
| @@ -28,20 +28,41 @@ | |||||
| </div> | </div> | ||||
| <ul class="list-group settings-sortable" id="col-list"> | <ul class="list-group settings-sortable" id="col-list"> | ||||
| <?php foreach ($columns as $col): ?> | <?php foreach ($columns as $col): ?> | ||||
| <li class="list-group-item d-flex align-items-center gap-2 py-2" | |||||
| <li class="list-group-item py-2" | |||||
| data-id="<?= e((string) $col->id) ?>"> | data-id="<?= e((string) $col->id) ?>"> | ||||
| <i class="bi bi-grip-vertical text-muted drag-handle" style="cursor:grab;"></i> | |||||
| <span class="flex-grow-1 col-label-text"><?= e($col->name) ?></span> | |||||
| <div class="form-check form-switch m-0" title="Show card count in column header"> | |||||
| <input class="form-check-input col-count-toggle" type="checkbox" role="switch" | |||||
| <?= $col->showCardCount ? 'checked' : '' ?>> | |||||
| <div class="d-flex align-items-center gap-2"> | |||||
| <i class="bi bi-grip-vertical text-muted drag-handle" style="cursor:grab;"></i> | |||||
| <span class="flex-grow-1 col-label-text"><?= e($col->name) ?></span> | |||||
| <div class="form-check form-switch m-0" title="Show card count in column header"> | |||||
| <input class="form-check-input col-count-toggle" type="checkbox" role="switch" | |||||
| <?= $col->showCardCount ? 'checked' : '' ?>> | |||||
| </div> | |||||
| <button class="btn btn-sm btn-link p-0 text-secondary btn-toggle-col-age" title="Card age settings"> | |||||
| <i class="bi bi-clock-history"></i> | |||||
| </button> | |||||
| <button class="btn btn-sm btn-link p-0 text-secondary btn-edit-col" title="Rename"> | |||||
| <i class="bi bi-pencil"></i> | |||||
| </button> | |||||
| <button class="btn btn-sm btn-link p-0 text-danger btn-delete-col" title="Delete"> | |||||
| <i class="bi bi-trash"></i> | |||||
| </button> | |||||
| </div> | |||||
| <div class="col-age-settings d-none mt-2 ps-4"> | |||||
| <div class="form-check form-switch mb-1"> | |||||
| <input class="form-check-input col-age-toggle" type="checkbox" role="switch" | |||||
| id="col-age-toggle-<?= e((string) $col->id) ?>" | |||||
| <?= $col->showCardAge ? 'checked' : '' ?>> | |||||
| <label class="form-check-label small" for="col-age-toggle-<?= e((string) $col->id) ?>"> | |||||
| Show "time in cell" tooltip & mark overdue | |||||
| </label> | |||||
| </div> | |||||
| <div class="input-group input-group-sm" style="max-width: 140px;"> | |||||
| <input type="number" min="0" step="1" class="form-control col-age-days" | |||||
| value="<?= e((string) $col->cardAgeWarningDays) ?>" placeholder="0"> | |||||
| <span class="input-group-text">days</span> | |||||
| </div> | |||||
| <div class="form-text">Overdue after this many days. 0 = no overdue marking.</div> | |||||
| </div> | </div> | ||||
| <button class="btn btn-sm btn-link p-0 text-secondary btn-edit-col" title="Rename"> | |||||
| <i class="bi bi-pencil"></i> | |||||
| </button> | |||||
| <button class="btn btn-sm btn-link p-0 text-danger btn-delete-col" title="Delete"> | |||||
| <i class="bi bi-trash"></i> | |||||
| </button> | |||||
| </li> | </li> | ||||
| <?php endforeach; ?> | <?php endforeach; ?> | ||||
| </ul> | </ul> | ||||
| @@ -64,43 +85,41 @@ | |||||
| </div> | </div> | ||||
| <ul class="list-group settings-sortable" id="lane-list"> | <ul class="list-group settings-sortable" id="lane-list"> | ||||
| <?php foreach ($lanes as $lane): ?> | <?php foreach ($lanes as $lane): ?> | ||||
| <li class="list-group-item d-flex align-items-center gap-2 py-2" | |||||
| <li class="list-group-item py-2" | |||||
| data-id="<?= e((string) $lane->id) ?>"> | data-id="<?= e((string) $lane->id) ?>"> | ||||
| <i class="bi bi-grip-vertical text-muted drag-handle" style="cursor:grab;"></i> | |||||
| <span class="flex-grow-1 lane-label-text"><?= e($lane->name) ?></span> | |||||
| <button class="btn btn-sm btn-link p-0 text-secondary btn-edit-lane" title="Rename"> | |||||
| <i class="bi bi-pencil"></i> | |||||
| </button> | |||||
| <button class="btn btn-sm btn-link p-0 text-danger btn-delete-lane" title="Delete"> | |||||
| <i class="bi bi-trash"></i> | |||||
| </button> | |||||
| <div class="d-flex align-items-center gap-2"> | |||||
| <i class="bi bi-grip-vertical text-muted drag-handle" style="cursor:grab;"></i> | |||||
| <span class="flex-grow-1 lane-label-text"><?= e($lane->name) ?></span> | |||||
| <button class="btn btn-sm btn-link p-0 text-secondary btn-toggle-lane-age" title="Card age settings"> | |||||
| <i class="bi bi-clock-history"></i> | |||||
| </button> | |||||
| <button class="btn btn-sm btn-link p-0 text-secondary btn-edit-lane" title="Rename"> | |||||
| <i class="bi bi-pencil"></i> | |||||
| </button> | |||||
| <button class="btn btn-sm btn-link p-0 text-danger btn-delete-lane" title="Delete"> | |||||
| <i class="bi bi-trash"></i> | |||||
| </button> | |||||
| </div> | |||||
| <div class="lane-age-settings d-none mt-2 ps-4"> | |||||
| <div class="form-check form-switch mb-1"> | |||||
| <input class="form-check-input lane-age-toggle" type="checkbox" role="switch" | |||||
| id="lane-age-toggle-<?= e((string) $lane->id) ?>" | |||||
| <?= $lane->showCardAge ? 'checked' : '' ?>> | |||||
| <label class="form-check-label small" for="lane-age-toggle-<?= e((string) $lane->id) ?>"> | |||||
| Show "time in cell" tooltip & mark overdue | |||||
| </label> | |||||
| </div> | |||||
| <div class="input-group input-group-sm" style="max-width: 140px;"> | |||||
| <input type="number" min="0" step="1" class="form-control lane-age-days" | |||||
| value="<?= e((string) $lane->cardAgeWarningDays) ?>" placeholder="0"> | |||||
| <span class="input-group-text">days</span> | |||||
| </div> | |||||
| <div class="form-text">Overdue after this many days. 0 = no overdue marking.</div> | |||||
| </div> | |||||
| </li> | </li> | ||||
| <?php endforeach; ?> | <?php endforeach; ?> | ||||
| </ul> | </ul> | ||||
| </div> | </div> | ||||
| <!-- Card age section --> | |||||
| <div class="mb-2"> | |||||
| <strong class="small d-block mb-2">Card Age</strong> | |||||
| <div class="form-check form-switch mb-2"> | |||||
| <input class="form-check-input" type="checkbox" role="switch" id="card-age-tooltip-toggle" | |||||
| <?= $board->showCardAge ? 'checked' : '' ?>> | |||||
| <label class="form-check-label small" for="card-age-tooltip-toggle"> | |||||
| Show "time in column/lane" tooltip on cards | |||||
| </label> | |||||
| </div> | |||||
| <label for="card-age-warning-days" class="form-label small mb-1"> | |||||
| Mark cards as overdue after this many days in a column/lane | |||||
| </label> | |||||
| <div class="input-group input-group-sm" style="max-width: 160px;"> | |||||
| <input type="number" min="0" step="1" class="form-control" id="card-age-warning-days" | |||||
| value="<?= e((string) $board->cardAgeWarningDays) ?>" placeholder="0"> | |||||
| <span class="input-group-text">days</span> | |||||
| </div> | |||||
| <div class="form-text">Set to 0 to disable.</div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -0,0 +1,24 @@ | |||||
| <?php | |||||
| declare(strict_types=1); | |||||
| use Core\Database; | |||||
| use Core\Migration; | |||||
| return new class extends Migration | |||||
| { | |||||
| public function up(Database $database): void | |||||
| { | |||||
| $database->execute( | |||||
| 'ALTER TABLE board_columns ADD COLUMN show_card_age INTEGER NOT NULL DEFAULT 0' | |||||
| ); | |||||
| $database->execute( | |||||
| 'ALTER TABLE board_columns ADD COLUMN card_age_warning_days INTEGER NOT NULL DEFAULT 0' | |||||
| ); | |||||
| } | |||||
| public function down(Database $database): void | |||||
| { | |||||
| // SQLite cannot drop columns without recreating the table. | |||||
| } | |||||
| }; | |||||
| @@ -0,0 +1,24 @@ | |||||
| <?php | |||||
| declare(strict_types=1); | |||||
| use Core\Database; | |||||
| use Core\Migration; | |||||
| return new class extends Migration | |||||
| { | |||||
| public function up(Database $database): void | |||||
| { | |||||
| $database->execute( | |||||
| 'ALTER TABLE swim_lanes ADD COLUMN show_card_age INTEGER NOT NULL DEFAULT 0' | |||||
| ); | |||||
| $database->execute( | |||||
| 'ALTER TABLE swim_lanes ADD COLUMN card_age_warning_days INTEGER NOT NULL DEFAULT 0' | |||||
| ); | |||||
| } | |||||
| public function down(Database $database): void | |||||
| { | |||||
| // SQLite cannot drop columns without recreating the table. | |||||
| } | |||||
| }; | |||||
| @@ -0,0 +1,25 @@ | |||||
| <?php | |||||
| declare(strict_types=1); | |||||
| use Core\Database; | |||||
| use Core\Migration; | |||||
| return new class extends Migration | |||||
| { | |||||
| /** | |||||
| * Card age tooltip/overdue settings moved to board_columns and swim_lanes | |||||
| * (configurable per column and per swim lane instead of per board). | |||||
| */ | |||||
| public function up(Database $database): void | |||||
| { | |||||
| $database->execute('ALTER TABLE boards DROP COLUMN show_card_age'); | |||||
| $database->execute('ALTER TABLE boards DROP COLUMN card_age_warning_days'); | |||||
| } | |||||
| public function down(Database $database): void | |||||
| { | |||||
| $database->execute('ALTER TABLE boards ADD COLUMN show_card_age INTEGER NOT NULL DEFAULT 0'); | |||||
| $database->execute('ALTER TABLE boards ADD COLUMN card_age_warning_days INTEGER NOT NULL DEFAULT 0'); | |||||
| } | |||||
| }; | |||||
| @@ -6,6 +6,8 @@ | |||||
| var laneCollapseStorageKey = 'kanban_lane_collapsed_' + String(boardId); | var laneCollapseStorageKey = 'kanban_lane_collapsed_' + String(boardId); | ||||
| var collapsedLaneIds = loadCollapsedLaneIds(); | var collapsedLaneIds = loadCollapsedLaneIds(); | ||||
| var columnShowCount = {}; | var columnShowCount = {}; | ||||
| var columnAgeSettings = {}; | |||||
| var laneAgeSettings = {}; | |||||
| var searchState = { | var searchState = { | ||||
| query: '' | query: '' | ||||
| }; | }; | ||||
| @@ -69,8 +71,26 @@ | |||||
| return 'less than an hour'; | return 'less than an hour'; | ||||
| } | } | ||||
| function effectiveCardAgeSettings(card) { | |||||
| var colSettings = columnAgeSettings[String(card.column_id)] || { showCardAge: false, cardAgeWarningDays: 0 }; | |||||
| var laneSettings = laneAgeSettings[String(card.swim_lane_id)] || { showCardAge: false, cardAgeWarningDays: 0 }; | |||||
| var showCardAge = colSettings.showCardAge || laneSettings.showCardAge; | |||||
| var warningDays = 0; | |||||
| if (colSettings.showCardAge) { | |||||
| warningDays = colSettings.cardAgeWarningDays; | |||||
| } else if (laneSettings.showCardAge) { | |||||
| warningDays = laneSettings.cardAgeWarningDays; | |||||
| } | |||||
| return { showCardAge: showCardAge, cardAgeWarningDays: warningDays }; | |||||
| } | |||||
| function applyCardAge(el, card) { | function applyCardAge(el, card) { | ||||
| if (KANBAN.showCardAge) { | |||||
| var settings = effectiveCardAgeSettings(card); | |||||
| if (settings.showCardAge) { | |||||
| var age = formatAge(card.cell_entered_at); | var age = formatAge(card.cell_entered_at); | ||||
| if (age) { | if (age) { | ||||
| el.title = 'In this column/lane for ' + age; | el.title = 'In this column/lane for ' + age; | ||||
| @@ -81,8 +101,8 @@ | |||||
| el.removeAttribute('title'); | el.removeAttribute('title'); | ||||
| } | } | ||||
| var isOverdue = KANBAN.cardAgeWarningDays > 0 && | |||||
| ageInDays(card.cell_entered_at) >= KANBAN.cardAgeWarningDays; | |||||
| var isOverdue = settings.cardAgeWarningDays > 0 && | |||||
| ageInDays(card.cell_entered_at) >= settings.cardAgeWarningDays; | |||||
| el.classList.toggle('kanban-card-overdue', isOverdue); | el.classList.toggle('kanban-card-overdue', isOverdue); | ||||
| } | } | ||||
| @@ -132,6 +152,21 @@ | |||||
| }); | }); | ||||
| } | } | ||||
| function initCardAgeSettings() { | |||||
| KANBAN_COLS.forEach(function (col) { | |||||
| columnAgeSettings[String(col.id)] = { | |||||
| showCardAge: !!col.show_card_age, | |||||
| cardAgeWarningDays: parseInt(col.card_age_warning_days, 10) || 0 | |||||
| }; | |||||
| }); | |||||
| KANBAN_LANES.forEach(function (lane) { | |||||
| laneAgeSettings[String(lane.id)] = { | |||||
| showCardAge: !!lane.show_card_age, | |||||
| cardAgeWarningDays: parseInt(lane.card_age_warning_days, 10) || 0 | |||||
| }; | |||||
| }); | |||||
| } | |||||
| function loadCollapsedLaneIds() { | function loadCollapsedLaneIds() { | ||||
| var laneMap = {}; | var laneMap = {}; | ||||
| try { | try { | ||||
| @@ -470,6 +505,7 @@ | |||||
| grid.insertBefore(hdr, refNode); | grid.insertBefore(hdr, refNode); | ||||
| columnShowCount[String(col.id)] = false; | columnShowCount[String(col.id)] = false; | ||||
| columnAgeSettings[String(col.id)] = { showCardAge: false, cardAgeWarningDays: 0 }; | |||||
| var laneHeaders = grid.querySelectorAll('.kanban-lane-header'); | var laneHeaders = grid.querySelectorAll('.kanban-lane-header'); | ||||
| laneHeaders.forEach(function (lh) { | laneHeaders.forEach(function (lh) { | ||||
| @@ -490,6 +526,7 @@ | |||||
| document.querySelectorAll('.kanban-cell[data-col-id="' + colId + '"]').forEach(function (el) { el.remove(); }); | document.querySelectorAll('.kanban-cell[data-col-id="' + colId + '"]').forEach(function (el) { el.remove(); }); | ||||
| KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.column_id) !== String(colId); }); | KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.column_id) !== String(colId); }); | ||||
| delete columnShowCount[String(colId)]; | delete columnShowCount[String(colId)]; | ||||
| delete columnAgeSettings[String(colId)]; | |||||
| applyGridTemplate(); | applyGridTemplate(); | ||||
| }, | }, | ||||
| addLane: function (lane) { | addLane: function (lane) { | ||||
| @@ -507,6 +544,8 @@ | |||||
| grid.appendChild(lh); | grid.appendChild(lh); | ||||
| bindLaneHeaderToggle(lh); | bindLaneHeaderToggle(lh); | ||||
| laneAgeSettings[String(lane.id)] = { showCardAge: false, cardAgeWarningDays: 0 }; | |||||
| colHeaders.forEach(function (ch) { | colHeaders.forEach(function (ch) { | ||||
| var cell = document.createElement('div'); | var cell = document.createElement('div'); | ||||
| cell.className = 'kanban-cell'; | cell.className = 'kanban-cell'; | ||||
| @@ -526,6 +565,7 @@ | |||||
| document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').remove(); | document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').remove(); | ||||
| document.querySelectorAll('.kanban-cell[data-lane-id="' + laneId + '"]').forEach(function (el) { el.remove(); }); | document.querySelectorAll('.kanban-cell[data-lane-id="' + laneId + '"]').forEach(function (el) { el.remove(); }); | ||||
| KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.swim_lane_id) !== String(laneId); }); | KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.swim_lane_id) !== String(laneId); }); | ||||
| delete laneAgeSettings[String(laneId)]; | |||||
| if (collapsedLaneIds[String(laneId)]) { | if (collapsedLaneIds[String(laneId)]) { | ||||
| delete collapsedLaneIds[String(laneId)]; | delete collapsedLaneIds[String(laneId)]; | ||||
| saveCollapsedLaneIds(); | saveCollapsedLaneIds(); | ||||
| @@ -543,14 +583,24 @@ | |||||
| columnShowCount[String(colId)] = show; | columnShowCount[String(colId)] = show; | ||||
| refreshColumnCount(colId); | refreshColumnCount(colId); | ||||
| }, | }, | ||||
| setCardAgeSettings: function (showCardAge, cardAgeWarningDays) { | |||||
| KANBAN.showCardAge = !!showCardAge; | |||||
| KANBAN.cardAgeWarningDays = parseInt(cardAgeWarningDays, 10) || 0; | |||||
| setColumnCardAge: function (colId, showCardAge, cardAgeWarningDays) { | |||||
| columnAgeSettings[String(colId)] = { | |||||
| showCardAge: !!showCardAge, | |||||
| cardAgeWarningDays: parseInt(cardAgeWarningDays, 10) || 0 | |||||
| }; | |||||
| refreshAllCardAges(); | |||||
| }, | |||||
| setLaneCardAge: function (laneId, showCardAge, cardAgeWarningDays) { | |||||
| laneAgeSettings[String(laneId)] = { | |||||
| showCardAge: !!showCardAge, | |||||
| cardAgeWarningDays: parseInt(cardAgeWarningDays, 10) || 0 | |||||
| }; | |||||
| refreshAllCardAges(); | refreshAllCardAges(); | ||||
| } | } | ||||
| }; | }; | ||||
| applyGridTemplate(); | applyGridTemplate(); | ||||
| initCardAgeSettings(); | |||||
| renderCards(); | renderCards(); | ||||
| initSortables(); | initSortables(); | ||||
| initJobSearch(); | initJobSearch(); | ||||
| @@ -45,22 +45,72 @@ | |||||
| }); | }); | ||||
| } | } | ||||
| function buildListItem(id, name, editClass, deleteClass, labelClass, countToggle) { | |||||
| function buildListItem(id, name, editClass, deleteClass, labelClass, countToggle, agePrefix) { | |||||
| var li = document.createElement('li'); | var li = document.createElement('li'); | ||||
| li.className = 'list-group-item d-flex align-items-center gap-2 py-2'; | |||||
| li.className = 'list-group-item py-2'; | |||||
| li.dataset.id = id; | li.dataset.id = id; | ||||
| li.innerHTML = | li.innerHTML = | ||||
| '<i class="bi bi-grip-vertical text-muted drag-handle" style="cursor:grab;"></i>' + | |||||
| '<span class="flex-grow-1 ' + labelClass + '">' + esc(name) + '</span>' + | |||||
| (countToggle ? | |||||
| '<div class="form-check form-switch m-0" title="Show card count in column header">' + | |||||
| '<input class="form-check-input col-count-toggle" type="checkbox" role="switch">' + | |||||
| '</div>' : '') + | |||||
| '<button class="btn btn-sm btn-link p-0 text-secondary ' + editClass + '" title="Rename"><i class="bi bi-pencil"></i></button>' + | |||||
| '<button class="btn btn-sm btn-link p-0 text-danger ' + deleteClass + '" title="Delete"><i class="bi bi-trash"></i></button>'; | |||||
| '<div class="d-flex align-items-center gap-2">' + | |||||
| '<i class="bi bi-grip-vertical text-muted drag-handle" style="cursor:grab;"></i>' + | |||||
| '<span class="flex-grow-1 ' + labelClass + '">' + esc(name) + '</span>' + | |||||
| (countToggle ? | |||||
| '<div class="form-check form-switch m-0" title="Show card count in column header">' + | |||||
| '<input class="form-check-input col-count-toggle" type="checkbox" role="switch">' + | |||||
| '</div>' : '') + | |||||
| '<button class="btn btn-sm btn-link p-0 text-secondary btn-toggle-' + agePrefix + '-age" title="Card age settings"><i class="bi bi-clock-history"></i></button>' + | |||||
| '<button class="btn btn-sm btn-link p-0 text-secondary ' + editClass + '" title="Rename"><i class="bi bi-pencil"></i></button>' + | |||||
| '<button class="btn btn-sm btn-link p-0 text-danger ' + deleteClass + '" title="Delete"><i class="bi bi-trash"></i></button>' + | |||||
| '</div>' + | |||||
| '<div class="' + agePrefix + '-age-settings d-none mt-2 ps-4">' + | |||||
| '<div class="form-check form-switch mb-1">' + | |||||
| '<input class="form-check-input ' + agePrefix + '-age-toggle" type="checkbox" role="switch">' + | |||||
| '<label class="form-check-label small">Show "time in cell" tooltip & mark overdue</label>' + | |||||
| '</div>' + | |||||
| '<div class="input-group input-group-sm" style="max-width: 140px;">' + | |||||
| '<input type="number" min="0" step="1" class="form-control ' + agePrefix + '-age-days" value="0" placeholder="0">' + | |||||
| '<span class="input-group-text">days</span>' + | |||||
| '</div>' + | |||||
| '<div class="form-text">Overdue after this many days. 0 = no overdue marking.</div>' + | |||||
| '</div>'; | |||||
| return li; | return li; | ||||
| } | } | ||||
| /* ── Card age settings (per column / per swim lane) ────────── */ | |||||
| function bindAgeSettings(li, prefix, urlBase, setter) { | |||||
| var toggleBtn = li.querySelector('.btn-toggle-' + prefix + '-age'); | |||||
| var settingsDiv = li.querySelector('.' + prefix + '-age-settings'); | |||||
| if (toggleBtn && settingsDiv) { | |||||
| toggleBtn.addEventListener('click', function () { | |||||
| settingsDiv.classList.toggle('d-none'); | |||||
| }); | |||||
| } | |||||
| var ageToggle = li.querySelector('.' + prefix + '-age-toggle'); | |||||
| var ageDays = li.querySelector('.' + prefix + '-age-days'); | |||||
| if (!ageToggle || !ageDays) return; | |||||
| function save() { | |||||
| var show = ageToggle.checked; | |||||
| var days = parseInt(ageDays.value, 10); | |||||
| if (isNaN(days) || days < 0) days = 0; | |||||
| ageDays.value = days; | |||||
| post(urlBase + li.dataset.id + '/card-age-settings', { | |||||
| show_card_age: show ? '1' : '0', | |||||
| card_age_warning_days: days | |||||
| }, function (res) { | |||||
| if (!res.ok) { | |||||
| alert(res.error || 'Update failed'); | |||||
| return; | |||||
| } | |||||
| setter(li.dataset.id, res.show_card_age, res.card_age_warning_days); | |||||
| }); | |||||
| } | |||||
| ageToggle.addEventListener('change', save); | |||||
| ageDays.addEventListener('change', save); | |||||
| } | |||||
| function esc(s) { | function esc(s) { | ||||
| return String(s) | return String(s) | ||||
| .replace(/&/g, '&').replace(/</g, '<') | .replace(/&/g, '&').replace(/</g, '<') | ||||
| @@ -104,7 +154,7 @@ | |||||
| if (!res.ok) { alert(res.error || 'Failed'); return; } | if (!res.ok) { alert(res.error || 'Failed'); return; } | ||||
| document.getElementById('col-add-form').classList.add('d-none'); | document.getElementById('col-add-form').classList.add('d-none'); | ||||
| document.getElementById('col-add-input').value = ''; | document.getElementById('col-add-input').value = ''; | ||||
| var li = buildListItem(res.id, res.name, 'btn-edit-col', 'btn-delete-col', 'col-label-text', true); | |||||
| var li = buildListItem(res.id, res.name, 'btn-edit-col', 'btn-delete-col', 'col-label-text', true, 'col'); | |||||
| document.getElementById('col-list').appendChild(li); | document.getElementById('col-list').appendChild(li); | ||||
| bindColItem(li); | bindColItem(li); | ||||
| window.KanbanBoard.addColumn(res); | window.KanbanBoard.addColumn(res); | ||||
| @@ -151,6 +201,7 @@ | |||||
| }); | }); | ||||
| }); | }); | ||||
| } | } | ||||
| bindAgeSettings(li, 'col', '/columns/', window.KanbanBoard.setColumnCardAge); | |||||
| } | } | ||||
| document.querySelectorAll('#col-list li').forEach(bindColItem); | document.querySelectorAll('#col-list li').forEach(bindColItem); | ||||
| @@ -175,7 +226,7 @@ | |||||
| if (!res.ok) { alert(res.error || 'Failed'); return; } | if (!res.ok) { alert(res.error || 'Failed'); return; } | ||||
| document.getElementById('lane-add-form').classList.add('d-none'); | document.getElementById('lane-add-form').classList.add('d-none'); | ||||
| document.getElementById('lane-add-input').value = ''; | document.getElementById('lane-add-input').value = ''; | ||||
| var li = buildListItem(res.id, res.name, 'btn-edit-lane', 'btn-delete-lane', 'lane-label-text'); | |||||
| var li = buildListItem(res.id, res.name, 'btn-edit-lane', 'btn-delete-lane', 'lane-label-text', false, 'lane'); | |||||
| document.getElementById('lane-list').appendChild(li); | document.getElementById('lane-list').appendChild(li); | ||||
| bindLaneItem(li); | bindLaneItem(li); | ||||
| window.KanbanBoard.addLane(res); | window.KanbanBoard.addLane(res); | ||||
| @@ -208,38 +259,11 @@ | |||||
| } | } | ||||
| }); | }); | ||||
| }); | }); | ||||
| bindAgeSettings(li, 'lane', '/swimlanes/', window.KanbanBoard.setLaneCardAge); | |||||
| } | } | ||||
| document.querySelectorAll('#lane-list li').forEach(bindLaneItem); | document.querySelectorAll('#lane-list li').forEach(bindLaneItem); | ||||
| /* ═══════════════════════════════════════════════════════════ | |||||
| CARD AGE | |||||
| ═══════════════════════════════════════════════════════════ */ | |||||
| var cardAgeTooltipToggle = document.getElementById('card-age-tooltip-toggle'); | |||||
| var cardAgeWarningDays = document.getElementById('card-age-warning-days'); | |||||
| function saveCardAgeSettings() { | |||||
| var showCardAge = cardAgeTooltipToggle.checked; | |||||
| var warningDays = parseInt(cardAgeWarningDays.value, 10); | |||||
| if (isNaN(warningDays) || warningDays < 0) warningDays = 0; | |||||
| cardAgeWarningDays.value = warningDays; | |||||
| post('/boards/' + boardId + '/card-age-settings', { | |||||
| show_card_age: showCardAge ? '1' : '0', | |||||
| card_age_warning_days: warningDays | |||||
| }, function (res) { | |||||
| if (!res.ok) { | |||||
| alert(res.error || 'Update failed'); | |||||
| return; | |||||
| } | |||||
| window.KanbanBoard.setCardAgeSettings(res.show_card_age, res.card_age_warning_days); | |||||
| }); | |||||
| } | |||||
| cardAgeTooltipToggle.addEventListener('change', saveCardAgeSettings); | |||||
| cardAgeWarningDays.addEventListener('change', saveCardAgeSettings); | |||||
| /* ── Inline rename helper ─────────────────────────────────── */ | /* ── Inline rename helper ─────────────────────────────────── */ | ||||
| function startRename(li, labelSel, saveCb) { | function startRename(li, labelSel, saveCb) { | ||||
| var span = li.querySelector(labelSel); | var span = li.querySelector(labelSel); | ||||
| @@ -21,7 +21,6 @@ $router->get('/board/{slug}', [BoardsController::class, 'show']); | |||||
| $router->get('/board/{slug}/edit', [BoardsController::class, 'edit']); | $router->get('/board/{slug}/edit', [BoardsController::class, 'edit']); | ||||
| $router->post('/board/{slug}/update', [BoardsController::class, 'update']); | $router->post('/board/{slug}/update', [BoardsController::class, 'update']); | ||||
| $router->post('/board/{slug}/delete', [BoardsController::class, 'destroy']); | $router->post('/board/{slug}/delete', [BoardsController::class, 'destroy']); | ||||
| $router->post('/boards/{id}/card-age-settings', [BoardsController::class, 'updateCardAgeSettings']); | |||||
| // Cards (JSON API) | // Cards (JSON API) | ||||
| $router->post('/cards', [CardsController::class, 'store']); | $router->post('/cards', [CardsController::class, 'store']); | ||||
| @@ -32,12 +31,14 @@ $router->post('/cards/{id}', [CardsController::class, 'update']); | |||||
| // Columns (JSON API) — /columns/reorder MUST be before /columns/{id} | // Columns (JSON API) — /columns/reorder MUST be before /columns/{id} | ||||
| $router->post('/columns/reorder', [ColumnsController::class, 'reorder']); | $router->post('/columns/reorder', [ColumnsController::class, 'reorder']); | ||||
| $router->post('/columns/{id}/toggle-count', [ColumnsController::class, 'toggleCount']); | $router->post('/columns/{id}/toggle-count', [ColumnsController::class, 'toggleCount']); | ||||
| $router->post('/columns/{id}/card-age-settings', [ColumnsController::class, 'updateCardAgeSettings']); | |||||
| $router->post('/columns/{id}/delete', [ColumnsController::class, 'destroy']); | $router->post('/columns/{id}/delete', [ColumnsController::class, 'destroy']); | ||||
| $router->post('/columns/{id}', [ColumnsController::class, 'update']); | $router->post('/columns/{id}', [ColumnsController::class, 'update']); | ||||
| $router->post('/columns', [ColumnsController::class, 'store']); | $router->post('/columns', [ColumnsController::class, 'store']); | ||||
| // Swim lanes (JSON API) — /swimlanes/reorder MUST be before /swimlanes/{id} | // Swim lanes (JSON API) — /swimlanes/reorder MUST be before /swimlanes/{id} | ||||
| $router->post('/swimlanes/reorder', [SwimLanesController::class, 'reorder']); | $router->post('/swimlanes/reorder', [SwimLanesController::class, 'reorder']); | ||||
| $router->post('/swimlanes/{id}/card-age-settings', [SwimLanesController::class, 'updateCardAgeSettings']); | |||||
| $router->post('/swimlanes/{id}/delete', [SwimLanesController::class, 'destroy']); | $router->post('/swimlanes/{id}/delete', [SwimLanesController::class, 'destroy']); | ||||
| $router->post('/swimlanes/{id}', [SwimLanesController::class, 'update']); | $router->post('/swimlanes/{id}', [SwimLanesController::class, 'update']); | ||||
| $router->post('/swimlanes', [SwimLanesController::class, 'store']); | $router->post('/swimlanes', [SwimLanesController::class, 'store']); | ||||
Powered by TurnKey Linux.