diff --git a/app/Http/Controllers/ColumnController.php b/app/Http/Controllers/ColumnController.php
index 14bbd56..86b7eb6 100644
--- a/app/Http/Controllers/ColumnController.php
+++ b/app/Http/Controllers/ColumnController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
+use App\Http\Requests\ColumnSequenceUpdateRequest;
use App\Http\Requests\ColumnStoreRequest;
use App\Http\Requests\ColumnUpdateRequest;
use App\Models\Column;
@@ -43,4 +44,15 @@ public function destroy(Request $request, Column $column): RedirectResponse
return back();
}
+
+ public function updateSequence(ColumnSequenceUpdateRequest $request, Column $column): RedirectResponse
+ {
+ if ($column->team_id !== $request->user()->team_id) {
+ abort(403);
+ }
+
+ $this->columnService->updateSequence($column, $request->validated()['order']);
+
+ return back();
+ }
}
diff --git a/app/Http/Requests/ColumnSequenceUpdateRequest.php b/app/Http/Requests/ColumnSequenceUpdateRequest.php
new file mode 100644
index 0000000..e7969fe
--- /dev/null
+++ b/app/Http/Requests/ColumnSequenceUpdateRequest.php
@@ -0,0 +1,20 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'order' => ['required', 'integer', 'min:0'],
+ ];
+ }
+}
diff --git a/app/Services/ColumnService.php b/app/Services/ColumnService.php
index b52e373..1e6a08f 100644
--- a/app/Services/ColumnService.php
+++ b/app/Services/ColumnService.php
@@ -4,6 +4,7 @@
use App\Models\Column;
use App\Models\User;
+use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class ColumnService
@@ -31,6 +32,42 @@ public function updateColumn(Column $column, array $data): bool
return $column->update($data);
}
+ /**
+ * Update the sequence order of a column.
+ */
+ public function updateSequence(Column $column, int $newOrder): void
+ {
+ DB::transaction(function () use ($column, $newOrder): void {
+ $columns = Column::query()
+ ->where('team_id', $column->team_id)
+ ->orderBy('order')
+ ->orderBy('id')
+ ->lockForUpdate()
+ ->get(['id']);
+
+ $orderedIds = $columns->pluck('id')->values()->all();
+
+ $currentIndex = array_search($column->id, $orderedIds, true);
+ if ($currentIndex === false) {
+ return;
+ }
+
+ $targetIndex = max(0, min($newOrder, count($orderedIds) - 1));
+ if ($targetIndex === $currentIndex) {
+ return;
+ }
+
+ array_splice($orderedIds, $currentIndex, 1);
+ array_splice($orderedIds, $targetIndex, 0, [$column->id]);
+
+ foreach ($orderedIds as $index => $columnId) {
+ Column::query()
+ ->whereKey($columnId)
+ ->update(['order' => $index]);
+ }
+ });
+ }
+
/**
* Delete a column.
*/
diff --git a/resources/js/components/tasks/KanbanBoard.vue b/resources/js/components/tasks/KanbanBoard.vue
index 9d25bf5..afd059e 100644
--- a/resources/js/components/tasks/KanbanBoard.vue
+++ b/resources/js/components/tasks/KanbanBoard.vue
@@ -1,10 +1,13 @@
diff --git a/resources/js/components/tasks/KanbanColumn.vue b/resources/js/components/tasks/KanbanColumn.vue
index bd7785b..e252659 100644
--- a/resources/js/components/tasks/KanbanColumn.vue
+++ b/resources/js/components/tasks/KanbanColumn.vue
@@ -3,7 +3,14 @@ import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import type { Column, Task } from '@/types';
import { router } from '@inertiajs/vue3';
-import { ChevronDown, Pencil, Trash2 } from 'lucide-vue-next';
+import {
+ Check,
+ ChevronDown,
+ GripVertical,
+ Pencil,
+ Trash2,
+ X,
+} from 'lucide-vue-next';
import { ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import draggable from 'vuedraggable';
@@ -23,7 +30,7 @@ const editName = ref(props.column.name);
const localTasks = ref([...(props.column.tasks || [])]);
const pagination = ref(
- props.column.pagination ? { ...props.column.pagination } : undefined
+ props.column.pagination ? { ...props.column.pagination } : undefined,
);
const isLoadingMore = ref(false);
@@ -34,7 +41,7 @@ watch(
if (props.column.pagination) {
pagination.value = { ...props.column.pagination };
}
- }
+ },
);
const loadMoreTasks = async () => {
@@ -136,12 +143,35 @@ const onDragChange = (event: any) => {
@blur="saveColumnName"
autoFocus
/>
+
+
+
{{ column.name }}
group(function () {
Route::post('columns', 'store')->name('columns.store');
Route::put('columns/{column}', 'update')->name('columns.update');
+ Route::put('columns/{column}/sequence', 'updateSequence')->name('columns.sequence.update');
Route::delete('columns/{column}', 'destroy')->name('columns.destroy');
});
diff --git a/tests/Feature/ColumnTest.php b/tests/Feature/ColumnTest.php
index 3397c22..9e3a3ad 100644
--- a/tests/Feature/ColumnTest.php
+++ b/tests/Feature/ColumnTest.php
@@ -36,3 +36,51 @@
$this->assertDatabaseMissing('columns', ['id' => $column->id]);
});
});
+
+describe('sequence update', function () {
+ test('can move a column to a new position', function () {
+ $first = Column::create(['team_id' => $this->team->id, 'name' => 'Todo', 'order' => 0]);
+ $second = Column::create(['team_id' => $this->team->id, 'name' => 'Progress', 'order' => 1]);
+ $third = Column::create(['team_id' => $this->team->id, 'name' => 'Done', 'order' => 2]);
+
+ $this->actingAs($this->user)
+ ->put(route('columns.sequence.update', $first), [
+ 'order' => 1,
+ ])
+ ->assertRedirect();
+
+ $this->assertDatabaseHas('columns', ['id' => $second->id, 'order' => 0]);
+ $this->assertDatabaseHas('columns', ['id' => $first->id, 'order' => 1]);
+ $this->assertDatabaseHas('columns', ['id' => $third->id, 'order' => 2]);
+ });
+
+ test('cannot reorder a column from another team', function () {
+ $foreignColumn = Column::create(['team_id' => $this->otherTeam->id, 'name' => 'Foreign', 'order' => 0]);
+
+ $this->actingAs($this->user)
+ ->put(route('columns.sequence.update', $foreignColumn), [
+ 'order' => 0,
+ ])
+ ->assertStatus(403);
+ });
+
+ test('reorders correctly when existing orders are 1-based', function () {
+ $todo = Column::create(['team_id' => $this->team->id, 'name' => 'Todo', 'order' => 1]);
+ $progress = Column::create(['team_id' => $this->team->id, 'name' => 'Progress', 'order' => 2]);
+ $done = Column::create(['team_id' => $this->team->id, 'name' => 'Done', 'order' => 3]);
+
+ $this->actingAs($this->user)
+ ->put(route('columns.sequence.update', $todo), [
+ 'order' => 1,
+ ])
+ ->assertRedirect();
+
+ $orderedIds = Column::query()
+ ->where('team_id', $this->team->id)
+ ->orderBy('order')
+ ->pluck('id')
+ ->all();
+
+ expect($orderedIds)->toBe([$progress->id, $todo->id, $done->id]);
+ });
+});