Daniel Covington 1 неделю назад
Родитель
Сommit
26cef101d0
10 измененных файлов: 168 добавлений и 20 удалений
  1. +18
    -0
      app/Controllers/ColumnsController.php
  2. +10
    -8
      app/Models/BoardColumn.php
  3. +18
    -9
      app/Repositories/BoardColumnRepository.php
  4. +1
    -1
      app/Views/boards/show.php
  5. +4
    -0
      app/Views/partials/settings-panel.php
  6. +21
    -0
      database/migrations/20260615_000001_add_show_card_count_to_board_columns.php
  7. +25
    -0
      public/css/kanban.css
  8. +50
    -0
      public/js/kanban-board.js
  9. +20
    -2
      public/js/kanban-settings.js
  10. +1
    -0
      routes/web.php

+ 18
- 0
app/Controllers/ColumnsController.php Просмотреть файл

@@ -79,6 +79,24 @@ class ColumnsController extends Controller
return $this->json(['ok' => true]);
}

public function toggleCount(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);
}

$show = (string) $request->input('show_card_count', '0') === '1';

$this->columns()->updateShowCardCount($id, $show, date('Y-m-d H:i:s'), AuthService::getCurrentUsername());

return $this->json(['ok' => true, 'show_card_count' => $show]);
}

public function destroy(int $id): mixed
{
if (!AuthService::isLoggedIn()) {


+ 10
- 8
app/Models/BoardColumn.php Просмотреть файл

@@ -10,6 +10,7 @@ class BoardColumn
public int $boardId = 0;
public string $name = '';
public int $position = 0;
public bool $showCardCount = false;
public ?string $createdAt = null;
public string $createdBy = '';
public ?string $updatedAt = null;
@@ -18,14 +19,15 @@ class BoardColumn
public static function fromRow(array $row): 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->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'] ?? '');

return $model;
}


+ 18
- 9
app/Repositories/BoardColumnRepository.php Просмотреть файл

@@ -35,16 +35,17 @@ class BoardColumnRepository extends Repository
public function insert(BoardColumn $col): BoardColumn
{
$this->database->execute(
'INSERT INTO board_columns (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 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)',
[
'board_id' => $col->boardId,
'name' => $col->name,
'position' => $col->position,
'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,
'created_at' => $col->createdAt,
'created_by' => $col->createdBy,
'updated_at' => $col->updatedAt,
'updated_by' => $col->updatedBy,
]
);

@@ -62,6 +63,14 @@ class BoardColumnRepository extends Repository
);
}

public function updateShowCardCount(int $id, bool $show, string $updatedAt, string $updatedBy): void
{
$this->database->execute(
'UPDATE board_columns SET show_card_count = :show_card_count, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id',
['show_card_count' => $show ? 1 : 0, 'updated_at' => $updatedAt, 'updated_by' => $updatedBy, 'id' => $id]
);
}

public function updatePosition(int $id, int $position, string $updatedAt, string $updatedBy): void
{
$this->database->execute(


+ 1
- 1
app/Views/boards/show.php Просмотреть файл

@@ -86,7 +86,7 @@ var KANBAN = {
boardSlug: "<?= e($board->slug) ?>",
cards: <?= $cardsJson ?>
};
var KANBAN_COLS = <?= json_encode(array_map(fn($c) => ['id' => $c->id, 'name' => $c->name, 'position' => $c->position], $columns)) ?>;
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)) ?>;
</script>



+ 4
- 0
app/Views/partials/settings-panel.php Просмотреть файл

@@ -32,6 +32,10 @@
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>
<button class="btn btn-sm btn-link p-0 text-secondary btn-edit-col" title="Rename">
<i class="bi bi-pencil"></i>
</button>


+ 21
- 0
database/migrations/20260615_000001_add_show_card_count_to_board_columns.php Просмотреть файл

@@ -0,0 +1,21 @@
<?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_count INTEGER NOT NULL DEFAULT 0'
);
}

public function down(Database $database): void
{
// SQLite cannot drop columns without recreating the table.
}
};

+ 25
- 0
public/css/kanban.css Просмотреть файл

@@ -135,6 +135,10 @@ body.kanban-page .navbar .btn-outline-secondary:hover {
z-index: 30;
min-width: 230px;
padding: 0.75rem 0.88rem;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.4rem;
color: #eff5ff;
font-size: clamp(0.68rem, 0.16vw + 0.65rem, 0.78rem);
font-weight: 700;
@@ -148,6 +152,27 @@ body.kanban-page .navbar .btn-outline-secondary:hover {
border-left: 1px solid rgba(255, 255, 255, 0.16);
}

.kanban-col-header .col-label {
min-width: 0;
}

.kanban-col-header .col-count-badge {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
height: 1.5rem;
padding: 0 0.4rem;
border-radius: 999px;
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0;
text-transform: none;
color: #173864;
background: #eff5ff;
}

.kanban-lane-header {
position: sticky;
left: 0;


+ 50
- 0
public/js/kanban-board.js Просмотреть файл

@@ -5,6 +5,7 @@
var boardId = KANBAN.boardId;
var laneCollapseStorageKey = 'kanban_lane_collapsed_' + String(boardId);
var collapsedLaneIds = loadCollapsedLaneIds();
var columnShowCount = {};
var searchState = {
query: ''
};
@@ -44,6 +45,37 @@
grid.style.gridTemplateColumns = cols;
}

function countCardsInColumn(colId) {
var count = 0;
KANBAN.cards.forEach(function (c) {
if (String(c.column_id) === String(colId)) count++;
});
return count;
}

function refreshColumnCount(colId) {
var header = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]');
if (!header) return;
var badge = header.querySelector('.col-count-badge');
if (!columnShowCount[String(colId)]) {
if (badge) badge.remove();
return;
}
if (!badge) {
badge = document.createElement('span');
badge.className = 'col-count-badge';
header.appendChild(badge);
}
badge.textContent = countCardsInColumn(colId);
}

function initColumnCounts() {
KANBAN_COLS.forEach(function (col) {
columnShowCount[String(col.id)] = !!col.show_card_count;
refreshColumnCount(col.id);
});
}

function loadCollapsedLaneIds() {
var laneMap = {};
try {
@@ -204,6 +236,7 @@
});

var card = KANBAN.cards.find(function (c) { return String(c.id) === String(cardId); });
var oldColId = card ? card.column_id : null;
if (card) {
card.column_id = parseInt(newColId, 10);
card.swim_lane_id = parseInt(newLaneId, 10);
@@ -213,6 +246,11 @@
evt.item.dataset.columnId = newColId;
evt.item.dataset.laneId = newLaneId;

if (oldColId !== null && String(oldColId) !== String(newColId)) {
refreshColumnCount(oldColId);
}
refreshColumnCount(newColId);

post('/cards/' + cardId + '/move', {
column_id: newColId,
swim_lane_id: newLaneId,
@@ -328,6 +366,7 @@
cell.appendChild(buildCardEl(card));
}
applyCardFilter();
refreshColumnCount(card.column_id);
},
onCardUpdated: function (id, data) {
var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
@@ -348,10 +387,13 @@
applyCardFilter();
},
onCardDeleted: function (id) {
var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
var colId = card ? card.column_id : null;
KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.id) !== String(id); });
var el = document.querySelector('.kanban-card[data-id="' + id + '"]');
if (el) el.remove();
applyCardFilter();
if (colId !== null) refreshColumnCount(colId);
},
addColumn: function (col) {
var grid = document.getElementById('kanban-grid');
@@ -365,6 +407,8 @@
hdr.innerHTML = '<span class="col-label">' + esc(col.name) + '</span>';
grid.insertBefore(hdr, refNode);

columnShowCount[String(col.id)] = false;

var laneHeaders = grid.querySelectorAll('.kanban-lane-header');
laneHeaders.forEach(function (lh) {
var laneId = lh.dataset.laneId;
@@ -383,6 +427,7 @@
document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]').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); });
delete columnShowCount[String(colId)];
applyGridTemplate();
},
addLane: function (lane) {
@@ -431,6 +476,10 @@
renameLane: function (laneId, name) {
var hdr = document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"] .lane-label');
if (hdr) hdr.textContent = name;
},
setColumnShowCount: function (colId, show) {
columnShowCount[String(colId)] = show;
refreshColumnCount(colId);
}
};

@@ -439,4 +488,5 @@
initSortables();
initJobSearch();
initLaneHeaderToggles();
initColumnCounts();
})();

+ 20
- 2
public/js/kanban-settings.js Просмотреть файл

@@ -45,13 +45,17 @@
});
}

function buildListItem(id, name, editClass, deleteClass, labelClass) {
function buildListItem(id, name, editClass, deleteClass, labelClass, countToggle) {
var li = document.createElement('li');
li.className = 'list-group-item d-flex align-items-center gap-2 py-2';
li.dataset.id = id;
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>';
return li;
@@ -100,7 +104,7 @@
if (!res.ok) { alert(res.error || 'Failed'); return; }
document.getElementById('col-add-form').classList.add('d-none');
document.getElementById('col-add-input').value = '';
var li = buildListItem(res.id, res.name, 'btn-edit-col', 'btn-delete-col', 'col-label-text');
var li = buildListItem(res.id, res.name, 'btn-edit-col', 'btn-delete-col', 'col-label-text', true);
document.getElementById('col-list').appendChild(li);
bindColItem(li);
window.KanbanBoard.addColumn(res);
@@ -133,6 +137,20 @@
}
});
});
var countToggle = li.querySelector('.col-count-toggle');
if (countToggle) {
countToggle.addEventListener('change', function () {
var show = countToggle.checked;
post('/columns/' + li.dataset.id + '/toggle-count', { show_card_count: show ? '1' : '0' }, function (res) {
if (res.ok) {
window.KanbanBoard.setColumnShowCount(li.dataset.id, show);
} else {
countToggle.checked = !show;
alert(res.error || 'Update failed');
}
});
});
}
}

document.querySelectorAll('#col-list li').forEach(bindColItem);


+ 1
- 0
routes/web.php Просмотреть файл

@@ -30,6 +30,7 @@ $router->post('/cards/{id}', [CardsController::class, 'update']);

// Columns (JSON API) — /columns/reorder MUST be before /columns/{id}
$router->post('/columns/reorder', [ColumnsController::class, 'reorder']);
$router->post('/columns/{id}/toggle-count', [ColumnsController::class, 'toggleCount']);
$router->post('/columns/{id}/delete', [ColumnsController::class, 'destroy']);
$router->post('/columns/{id}', [ColumnsController::class, 'update']);
$router->post('/columns', [ColumnsController::class, 'store']);


Загрузка…
Отмена
Сохранить

Powered by TurnKey Linux.