Daniel Covington 5 дней назад
Родитель
Сommit
15c2b273a9
6 измененных файлов: 115 добавлений и 0 удалений
  1. +39
    -0
      app/Controllers/ColumnsController.php
  2. +39
    -0
      app/Controllers/SwimLanesController.php
  3. +22
    -0
      app/Repositories/CardRepository.php
  4. +6
    -0
      app/Views/partials/settings-panel.php
  5. +7
    -0
      public/js/kanban-settings.js
  6. +2
    -0
      routes/web.php

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

@@ -126,6 +126,45 @@ class ColumnsController extends Controller
]);
}

public function export(int $id): mixed
{
if (!AuthService::isLoggedIn()) {
return $this->redirect('/auth/login');
}

$row = $this->columns()->find($id);
if ($row === null) {
return new \Core\Response('Not found', 404);
}

$col = \App\Models\BoardColumn::fromRow($row);
$cards = $this->cards()->findByColumnId($id);

$out = fopen('php://memory', 'wb');
fputcsv($out, ['Job #', 'Job Name', 'Customer', 'Delivery Date', 'Quantity', 'Notes']);
foreach ($cards as $card) {
fputcsv($out, [
$card->jobNumber,
$card->jobName,
$card->customerName,
$card->deliveryDate ?? '',
$card->quantity ?? '',
$card->notes,
]);
}
rewind($out);
$csv = (string) stream_get_contents($out);
fclose($out);

$slug = trim((string) preg_replace('/[^a-z0-9]+/i', '-', $col->name), '-');
$filename = ($slug ?: 'column') . '-' . date('Y-m-d') . '.csv';

return new \Core\Response($csv, 200, [
'Content-Type' => 'text/csv; charset=utf-8',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
}

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


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

@@ -108,6 +108,45 @@ class SwimLanesController extends Controller
]);
}

public function export(int $id): mixed
{
if (!AuthService::isLoggedIn()) {
return $this->redirect('/auth/login');
}

$row = $this->lanes()->find($id);
if ($row === null) {
return new \Core\Response('Not found', 404);
}

$lane = \App\Models\SwimLane::fromRow($row);
$cards = $this->cards()->findBySwimLaneId($id);

$out = fopen('php://memory', 'wb');
fputcsv($out, ['Job #', 'Job Name', 'Customer', 'Delivery Date', 'Quantity', 'Notes']);
foreach ($cards as $card) {
fputcsv($out, [
$card->jobNumber,
$card->jobName,
$card->customerName,
$card->deliveryDate ?? '',
$card->quantity ?? '',
$card->notes,
]);
}
rewind($out);
$csv = (string) stream_get_contents($out);
fclose($out);

$slug = trim((string) preg_replace('/[^a-z0-9]+/i', '-', $lane->name), '-');
$filename = ($slug ?: 'lane') . '-' . date('Y-m-d') . '.csv';

return new \Core\Response($csv, 200, [
'Content-Type' => 'text/csv; charset=utf-8',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
}

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


+ 22
- 0
app/Repositories/CardRepository.php Просмотреть файл

@@ -29,6 +29,28 @@ class CardRepository extends Repository
return array_map(fn(array $r) => Card::fromRow($r), $rows);
}

/** @return Card[] */
public function findByColumnId(int $columnId): array
{
$rows = $this->database->query(
'SELECT * FROM cards WHERE column_id = :column_id ORDER BY swim_lane_id ASC, position ASC',
['column_id' => $columnId]
);

return array_map(fn(array $r) => Card::fromRow($r), $rows);
}

/** @return Card[] */
public function findBySwimLaneId(int $swimLaneId): array
{
$rows = $this->database->query(
'SELECT * FROM cards WHERE swim_lane_id = :swim_lane_id ORDER BY column_id ASC, position ASC',
['swim_lane_id' => $swimLaneId]
);

return array_map(fn(array $r) => Card::fromRow($r), $rows);
}

public function maxPosition(int $columnId, int $swimLaneId): int
{
$row = $this->database->first(


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

@@ -40,6 +40,9 @@
<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-export-col" title="Export to CSV">
<i class="bi bi-download"></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>
@@ -93,6 +96,9 @@
<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-export-lane" title="Export to CSV">
<i class="bi bi-download"></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>


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

@@ -58,6 +58,7 @@
'<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 btn-export-' + agePrefix + '" title="Export to CSV"><i class="bi bi-download"></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>' +
@@ -201,6 +202,9 @@
});
});
}
li.querySelector('.btn-export-col').addEventListener('click', function () {
window.location.href = '/columns/' + li.dataset.id + '/export';
});
bindAgeSettings(li, 'col', '/columns/', function (id, show, days) {
window.KanbanBoard.setColumnCardAge(id, show, days);
});
@@ -261,6 +265,9 @@
}
});
});
li.querySelector('.btn-export-lane').addEventListener('click', function () {
window.location.href = '/swimlanes/' + li.dataset.id + '/export';
});
bindAgeSettings(li, 'lane', '/swimlanes/', function (id, show, days) {
window.KanbanBoard.setLaneCardAge(id, show, days);
});


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

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

// Columns (JSON API) — /columns/reorder MUST be before /columns/{id}
$router->get('/columns/{id}/export', [ColumnsController::class, 'export']);
$router->post('/columns/reorder', [ColumnsController::class, 'reorder']);
$router->post('/columns/{id}/toggle-count', [ColumnsController::class, 'toggleCount']);
$router->post('/columns/{id}/card-age-settings', [ColumnsController::class, 'updateCardAgeSettings']);
@@ -37,6 +38,7 @@ $router->post('/columns/{id}', [ColumnsController::class, 'update']);
$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}/card-age-settings', [SwimLanesController::class, 'updateCardAgeSettings']);
$router->post('/swimlanes/{id}/delete', [SwimLanesController::class, 'destroy']);


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

Powered by TurnKey Linux.