Kaynağa Gözat

Card Count In Swim Lanes

master
Daniel Covington 7 saat önce
ebeveyn
işleme
630afd2517
10 değiştirilmiş dosya ile 137 ekleme ve 6 silme
  1. +18
    -0
      app/Controllers/SwimLanesController.php
  2. +2
    -0
      app/Models/SwimLane.php
  3. +11
    -2
      app/Repositories/SwimLaneRepository.php
  4. +1
    -1
      app/Views/boards/show.php
  5. +4
    -0
      app/Views/partials/settings-panel.php
  6. +21
    -0
      database/migrations/20260622_000001_add_show_card_count_to_swim_lanes.php
  7. +15
    -0
      public/css/kanban.css
  8. +47
    -0
      public/js/kanban-board.js
  9. +17
    -3
      public/js/kanban-settings.js
  10. +1
    -0
      routes/web.php

+ 18
- 0
app/Controllers/SwimLanesController.php Dosyayı Görüntüle

@@ -79,6 +79,24 @@ class SwimLanesController 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->lanes()->find($id);
if ($row === null) {
return $this->json(['ok' => false, 'error' => 'Not found'], 404);
}

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

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

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

public function toggleExport(Request $request, int $id): mixed
{
if (!AuthService::isLoggedIn()) {


+ 2
- 0
app/Models/SwimLane.php Dosyayı Görüntüle

@@ -10,6 +10,7 @@ class SwimLane
public int $boardId = 0;
public string $name = '';
public int $position = 0;
public bool $showCardCount = false;
public bool $showExportButton = false;
public bool $showCardAge = false;
public int $cardAgeWarningDays = 0;
@@ -25,6 +26,7 @@ class SwimLane
$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->showExportButton = (bool) ($row['show_export_button'] ?? false);
$model->showCardAge = (bool) ($row['show_card_age'] ?? false);
$model->cardAgeWarningDays = (int) ($row['card_age_warning_days'] ?? 0);


+ 11
- 2
app/Repositories/SwimLaneRepository.php Dosyayı Görüntüle

@@ -35,12 +35,13 @@ class SwimLaneRepository extends Repository
public function insert(SwimLane $lane): SwimLane
{
$this->database->execute(
'INSERT INTO swim_lanes (board_id, name, position, show_export_button, show_card_age, card_age_warning_days, created_at, created_by, updated_at, updated_by)
VALUES (:board_id, :name, :position, :show_export_button, :show_card_age, :card_age_warning_days, :created_at, :created_by, :updated_at, :updated_by)',
'INSERT INTO swim_lanes (board_id, name, position, show_card_count, show_export_button, show_card_age, card_age_warning_days, created_at, created_by, updated_at, updated_by)
VALUES (:board_id, :name, :position, :show_card_count, :show_export_button, :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,
'show_card_count' => $lane->showCardCount ? 1 : 0,
'show_export_button' => $lane->showExportButton ? 1 : 0,
'show_card_age' => $lane->showCardAge ? 1 : 0,
'card_age_warning_days' => $lane->cardAgeWarningDays,
@@ -65,6 +66,14 @@ class SwimLaneRepository extends Repository
);
}

public function updateShowCardCount(int $id, bool $show, string $updatedAt, string $updatedBy): void
{
$this->database->execute(
'UPDATE swim_lanes 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 updateShowExportButton(int $id, bool $show, string $updatedAt, string $updatedBy): void
{
$this->database->execute(


+ 1
- 1
app/Views/boards/show.php Dosyayı Görüntüle

@@ -93,7 +93,7 @@ var KANBAN = {
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, 'show_export_button' => $c->showExportButton, '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_export_button' => $l->showExportButton, 'show_card_age' => $l->showCardAge, 'card_age_warning_days' => $l->cardAgeWarningDays], $lanes)) ?>;
var KANBAN_LANES = <?= json_encode(array_map(fn($l) => ['id' => $l->id, 'name' => $l->name, 'position' => $l->position, 'show_card_count' => $l->showCardCount, 'show_export_button' => $l->showExportButton, 'show_card_age' => $l->showCardAge, 'card_age_warning_days' => $l->cardAgeWarningDays], $lanes)) ?>;
</script>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>


+ 4
- 0
app/Views/partials/settings-panel.php Dosyayı Görüntüle

@@ -94,6 +94,10 @@
<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>
<div class="form-check form-switch m-0" title="Show card count in swim lane header">
<input class="form-check-input lane-count-toggle" type="checkbox" role="switch"
<?= $lane->showCardCount ? 'checked' : '' ?>>
</div>
<div class="form-check form-switch m-0" title="Show export button on board">
<input class="form-check-input lane-export-toggle" type="checkbox" role="switch"
<?= $lane->showExportButton ? 'checked' : '' ?>>


+ 21
- 0
database/migrations/20260622_000001_add_show_card_count_to_swim_lanes.php Dosyayı Görüntüle

@@ -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 swim_lanes ADD COLUMN show_card_count INTEGER NOT NULL DEFAULT 0'
);
}

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

+ 15
- 0
public/css/kanban.css Dosyayı Görüntüle

@@ -173,6 +173,21 @@ body.kanban-page .navbar .btn-outline-secondary:hover {
background: #eff5ff;
}

.kanban-lane-header .lane-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;
color: #fff;
background: #1e4d8f;
}

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


+ 47
- 0
public/js/kanban-board.js Dosyayı Görüntüle

@@ -6,6 +6,7 @@
var laneCollapseStorageKey = 'kanban_lane_collapsed_' + String(boardId);
var collapsedLaneIds = loadCollapsedLaneIds();
var columnShowCount = {};
var laneShowCount = {};
var columnShowExport = {};
var laneShowExport = {};
var columnAgeSettings = {};
@@ -154,6 +155,37 @@
});
}

function countCardsInLane(laneId) {
var count = 0;
KANBAN.cards.forEach(function (c) {
if (String(c.swim_lane_id) === String(laneId)) count++;
});
return count;
}

function refreshLaneCount(laneId) {
var header = document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]');
if (!header) return;
var badge = header.querySelector('.lane-count-badge');
if (!laneShowCount[String(laneId)]) {
if (badge) badge.remove();
return;
}
if (!badge) {
badge = document.createElement('span');
badge.className = 'lane-count-badge';
header.appendChild(badge);
}
badge.textContent = countCardsInLane(laneId);
}

function initLaneCounts() {
KANBAN_LANES.forEach(function (lane) {
laneShowCount[String(lane.id)] = !!lane.show_card_count;
refreshLaneCount(lane.id);
});
}

function refreshColumnExport(colId) {
var header = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]');
if (!header) return;
@@ -397,6 +429,11 @@
}
refreshColumnCount(newColId);

if (oldLaneId !== null && String(oldLaneId) !== String(newLaneId)) {
refreshLaneCount(oldLaneId);
}
refreshLaneCount(newLaneId);

post('/cards/' + cardId + '/move', {
column_id: newColId,
swim_lane_id: newLaneId,
@@ -513,6 +550,7 @@
}
applyCardFilter();
refreshColumnCount(card.column_id);
refreshLaneCount(card.swim_lane_id);
},
onCardUpdated: function (id, data) {
var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
@@ -535,11 +573,13 @@
onCardDeleted: function (id) {
var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
var colId = card ? card.column_id : null;
var laneId = card ? card.swim_lane_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);
if (laneId !== null) refreshLaneCount(laneId);
},
addColumn: function (col) {
var grid = document.getElementById('kanban-grid');
@@ -595,6 +635,7 @@
grid.appendChild(lh);
bindLaneHeaderToggle(lh);

laneShowCount[String(lane.id)] = false;
laneShowExport[String(lane.id)] = false;
laneAgeSettings[String(lane.id)] = { showCardAge: false, cardAgeWarningDays: 0 };

@@ -617,6 +658,7 @@
document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').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); });
delete laneShowCount[String(laneId)];
delete laneShowExport[String(laneId)];
delete laneAgeSettings[String(laneId)];
if (collapsedLaneIds[String(laneId)]) {
@@ -636,6 +678,10 @@
columnShowCount[String(colId)] = show;
refreshColumnCount(colId);
},
setLaneShowCount: function (laneId, show) {
laneShowCount[String(laneId)] = show;
refreshLaneCount(laneId);
},
setColumnShowExport: function (colId, show) {
columnShowExport[String(colId)] = !!show;
refreshColumnExport(colId);
@@ -668,4 +714,5 @@
initJobSearch();
initLaneHeaderToggles();
initColumnCounts();
initLaneCounts();
})();

+ 17
- 3
public/js/kanban-settings.js Dosyayı Görüntüle

@@ -54,8 +54,8 @@
'<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 class="form-check form-switch m-0" title="Show card count in header">' +
'<input class="form-check-input ' + agePrefix + '-count-toggle" type="checkbox" role="switch">' +
'</div>' : '') +
'<div class="form-check form-switch m-0" title="Show export button on board">' +
'<input class="form-check-input ' + agePrefix + '-export-toggle" type="checkbox" role="switch">' +
@@ -245,7 +245,7 @@
if (!res.ok) { alert(res.error || 'Failed'); return; }
document.getElementById('lane-add-form').classList.add('d-none');
document.getElementById('lane-add-input').value = '';
var li = buildListItem(res.id, res.name, 'btn-edit-lane', 'btn-delete-lane', 'lane-label-text', false, 'lane');
var li = buildListItem(res.id, res.name, 'btn-edit-lane', 'btn-delete-lane', 'lane-label-text', true, 'lane');
document.getElementById('lane-list').appendChild(li);
bindLaneItem(li);
window.KanbanBoard.addLane(res);
@@ -278,6 +278,20 @@
}
});
});
var laneCountToggle = li.querySelector('.lane-count-toggle');
if (laneCountToggle) {
laneCountToggle.addEventListener('change', function () {
var show = laneCountToggle.checked;
post('/swimlanes/' + li.dataset.id + '/toggle-count', { show_card_count: show ? '1' : '0' }, function (res) {
if (res.ok) {
window.KanbanBoard.setLaneShowCount(li.dataset.id, show);
} else {
laneCountToggle.checked = !show;
alert(res.error || 'Update failed');
}
});
});
}
var laneExportToggle = li.querySelector('.lane-export-toggle');
if (laneExportToggle) {
laneExportToggle.addEventListener('change', function () {


+ 1
- 0
routes/web.php Dosyayı Görüntüle

@@ -41,6 +41,7 @@ $router->post('/columns', [ColumnsController::class, 'store']);
// Swim lanes (JSON API) — /swimlanes/reorder MUST be before /swimlanes/{id}
$router->get('/swimlanes/{id}/export', [SwimLanesController::class, 'export']);
$router->post('/swimlanes/reorder', [SwimLanesController::class, 'reorder']);
$router->post('/swimlanes/{id}/toggle-count', [SwimLanesController::class, 'toggleCount']);
$router->post('/swimlanes/{id}/toggle-export', [SwimLanesController::class, 'toggleExport']);
$router->post('/swimlanes/{id}/card-age-settings', [SwimLanesController::class, 'updateCardAgeSettings']);
$router->post('/swimlanes/{id}/delete', [SwimLanesController::class, 'destroy']);


Yükleniyor…
İptal
Kaydet

Powered by TurnKey Linux.