diff --git a/app/Http/Requests/TaskCreateRequest.php b/app/Http/Requests/TaskCreateRequest.php index 489bc70..7170cf9 100644 --- a/app/Http/Requests/TaskCreateRequest.php +++ b/app/Http/Requests/TaskCreateRequest.php @@ -15,6 +15,7 @@ public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], + 'description' => ['nullable', 'string'], 'due_date' => ['nullable', 'date'], 'column_id' => ['nullable', 'exists:columns,id'], ]; diff --git a/app/Http/Requests/TaskUpdateRequest.php b/app/Http/Requests/TaskUpdateRequest.php index 58ecf7b..3fb1c7e 100644 --- a/app/Http/Requests/TaskUpdateRequest.php +++ b/app/Http/Requests/TaskUpdateRequest.php @@ -15,8 +15,8 @@ public function rules(): array { return [ 'title' => ['sometimes', 'string', 'max:255'], - 'due_date' => ['sometimes', 'date'], - 'completed' => ['sometimes', 'boolean'], + 'description' => ['nullable', 'string', 'max:1000'], + 'due_date' => ['sometimes', 'nullable', 'date'], ]; } } diff --git a/app/Http/Resources/TaskResource.php b/app/Http/Resources/TaskResource.php index ca97331..296cc84 100644 --- a/app/Http/Resources/TaskResource.php +++ b/app/Http/Resources/TaskResource.php @@ -14,13 +14,17 @@ class TaskResource extends JsonResource */ public function toArray(Request $request): array { + // Calculate days cleanly or fallback to 0 if null + $daysInColumn = $this->column_updated_at ? (int) $this->column_updated_at->diffInDays(now()) : 0; + return [ 'id' => $this->id, 'team_id' => $this->team_id, 'column_id' => $this->column_id, 'order' => $this->order, 'title' => $this->title, - 'completed' => (bool)$this->completed, + 'description' => $this->description, + 'days_in_column' => $daysInColumn, 'due_date' => $this->due_date, 'created_by' => $this->created_by, 'creator' => $this->whenLoaded('creator', fn () => [ diff --git a/app/Jobs/IncrementTeamCompletedTasks.php b/app/Jobs/IncrementTeamCompletedTasks.php deleted file mode 100644 index 1c0cb73..0000000 --- a/app/Jobs/IncrementTeamCompletedTasks.php +++ /dev/null @@ -1,27 +0,0 @@ -where('id', $this->teamId)->increment('count_completed_tasks'); - } -} diff --git a/app/Models/Task.php b/app/Models/Task.php index 50374c2..78e0839 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -16,13 +16,14 @@ class Task extends Model * @var list */ protected $fillable = [ - 'title', - 'due_date', - 'completed', 'team_id', - 'created_by', 'column_id', + 'column_updated_at', 'order', + 'title', + 'description', + 'due_date', + 'created_by', ]; /** @@ -33,6 +34,7 @@ class Task extends Model protected function casts(): array { return [ + 'column_updated_at' => 'datetime', 'due_date' => 'datetime', ]; } diff --git a/app/Services/ColumnService.php b/app/Services/ColumnService.php index 6c0ddd8..b52e373 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\Validation\ValidationException; class ColumnService { @@ -35,6 +36,12 @@ public function updateColumn(Column $column, array $data): bool */ public function deleteColumn(Column $column): ?bool { + if ($column->tasks()->exists()) { + throw ValidationException::withMessages([ + 'column' => 'Cannot delete a column that contains tasks. Please move or delete the tasks first.', + ]); + } + return $column->delete(); } } diff --git a/app/Services/TaskService.php b/app/Services/TaskService.php index ac35dd2..2ea08c1 100644 --- a/app/Services/TaskService.php +++ b/app/Services/TaskService.php @@ -5,7 +5,6 @@ use App\Models\Task; use App\Models\Column; use App\Models\User; -use App\Jobs\IncrementTeamCompletedTasks; use Illuminate\Support\Facades\DB; class TaskService @@ -31,6 +30,7 @@ public function createTask(array $data, User $user): Task 'created_by' => $user->id, 'column_id' => $columnId, 'order' => $order, + 'column_updated_at' => now(), // Initialize column timing ])); } @@ -39,13 +39,7 @@ public function createTask(array $data, User $user): Task */ public function updateTask(Task $task, array $data): bool { - $updated = $task->update($data); - - if ($updated && isset($data['completed']) && $data['completed']) { - IncrementTeamCompletedTasks::dispatch($task->team_id); - } - - return $updated; + return $task->update($data); } /** @@ -55,9 +49,10 @@ public function updateSequence(Task $task, int $newColumnId, int $newOrder): voi { $oldColumnId = $task->column_id; $oldOrder = $task->order; + $isDifferentColumn = $oldColumnId != $newColumnId; - DB::transaction(function () use ($task, $newColumnId, $newOrder, $oldColumnId, $oldOrder) { - if ($oldColumnId == $newColumnId) { + DB::transaction(function () use ($task, $newColumnId, $newOrder, $oldColumnId, $oldOrder, $isDifferentColumn) { + if (!$isDifferentColumn) { // Moving within the same column if ($oldOrder < $newOrder) { Task::where('column_id', $newColumnId) @@ -79,10 +74,17 @@ public function updateSequence(Task $task, int $newColumnId, int $newOrder): voi ->increment('order'); } - $task->update([ + $updateData = [ 'column_id' => $newColumnId, 'order' => $newOrder, - ]); + ]; + + // If it moved to a new column, reset its timer + if ($isDifferentColumn) { + $updateData['column_updated_at'] = now(); + } + + $task->update($updateData); }); } diff --git a/database/factories/TeamFactory.php b/database/factories/TeamFactory.php index 3d9de9f..e443b4e 100644 --- a/database/factories/TeamFactory.php +++ b/database/factories/TeamFactory.php @@ -18,7 +18,6 @@ public function definition(): array { return [ 'name' => fake()->company(), - 'count_completed_tasks' => 0, ]; } } diff --git a/database/migrations/2026_03_22_183704_refactor_task_completion.php b/database/migrations/2026_03_22_183704_refactor_task_completion.php new file mode 100644 index 0000000..cb5292e --- /dev/null +++ b/database/migrations/2026_03_22_183704_refactor_task_completion.php @@ -0,0 +1,38 @@ +dropColumn('completed'); + $table->timestamp('column_updated_at')->nullable(); + }); + + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('count_completed_tasks'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tasks', function (Blueprint $table) { + $table->boolean('completed')->default(false); + $table->dropColumn('column_updated_at'); + }); + + Schema::table('teams', function (Blueprint $table) { + $table->integer('count_completed_tasks')->default(0); + }); + } +}; diff --git a/database/migrations/2026_03_22_193016_add_description_to_tasks_table.php b/database/migrations/2026_03_22_193016_add_description_to_tasks_table.php new file mode 100644 index 0000000..fe213b5 --- /dev/null +++ b/database/migrations/2026_03_22_193016_add_description_to_tasks_table.php @@ -0,0 +1,28 @@ +text('description')->nullable()->after('title'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tasks', function (Blueprint $table) { + $table->dropColumn('description'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index f384d11..4814f83 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,14 +2,13 @@ namespace Database\Seeders; -use App\Models\Task; -use App\Models\Team; +use Illuminate\Database\Seeder; use App\Models\User; +use App\Models\Team; +use App\Models\Task; use App\Models\Column; use Illuminate\Support\Str; -use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\DB; class DatabaseSeeder extends Seeder { @@ -18,7 +17,7 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - $teams = Team::factory(3)->create(['count_completed_tasks' => 0]); + $teams = Team::factory(3)->create(); $usersPerTeam = [3, 2, 1]; $userIndex = 1; @@ -37,17 +36,19 @@ public function run(): void $todoColumn = Column::create([ 'team_id' => $team->id, 'name' => 'To Do', - 'order' => 0, + 'order' => 1 ]); - $inProgressColumn = Column::create([ + + $progressColumn = Column::create([ 'team_id' => $team->id, 'name' => 'In Progress', - 'order' => 1, + 'order' => 2 ]); + $doneColumn = Column::create([ 'team_id' => $team->id, 'name' => 'Done', - 'order' => 2, + 'order' => 3 ]); $teamUsers = User::where('team_id', $team->id)->pluck('id'); @@ -62,14 +63,14 @@ public function run(): void $doneOrder = 0; foreach ($tasks as $task) { + // Randomly assign to a column $rand = rand(0, 2); if ($rand === 0) { - $task->update(['column_id' => $todoColumn->id, 'order' => $todoOrder++, 'completed' => false]); + $task->update(['column_id' => $todoColumn->id, 'order' => $todoOrder++, 'column_updated_at' => now()]); } elseif ($rand === 1) { - $task->update(['column_id' => $inProgressColumn->id, 'order' => $progressOrder++, 'completed' => false]); + $task->update(['column_id' => $progressColumn->id, 'order' => $progressOrder++, 'column_updated_at' => now()->subDays(rand(1, 5))]); } else { - $task->update(['column_id' => $doneColumn->id, 'order' => $doneOrder++, 'completed' => true]); - DB::table('teams')->where('id', $team->id)->increment('count_completed_tasks'); + $task->update(['column_id' => $doneColumn->id, 'order' => $doneOrder++, 'column_updated_at' => now()->subDays(rand(2, 10))]); } } } diff --git a/resources/js/components/tasks/ColumnCreateDialog.vue b/resources/js/components/tasks/ColumnCreateDialog.vue new file mode 100644 index 0000000..9a15365 --- /dev/null +++ b/resources/js/components/tasks/ColumnCreateDialog.vue @@ -0,0 +1,77 @@ + + + diff --git a/resources/js/components/tasks/ColumnDeleteDialog.vue b/resources/js/components/tasks/ColumnDeleteDialog.vue new file mode 100644 index 0000000..61ec04b --- /dev/null +++ b/resources/js/components/tasks/ColumnDeleteDialog.vue @@ -0,0 +1,44 @@ + + + diff --git a/resources/js/components/tasks/KanbanBoard.vue b/resources/js/components/tasks/KanbanBoard.vue index db20b18..9d25bf5 100644 --- a/resources/js/components/tasks/KanbanBoard.vue +++ b/resources/js/components/tasks/KanbanBoard.vue @@ -1,11 +1,10 @@ diff --git a/resources/js/components/tasks/KanbanColumn.vue b/resources/js/components/tasks/KanbanColumn.vue index a05f21f..fb4f2a0 100644 --- a/resources/js/components/tasks/KanbanColumn.vue +++ b/resources/js/components/tasks/KanbanColumn.vue @@ -3,6 +3,7 @@ import { ref, watch } from 'vue'; import draggable from 'vuedraggable'; import type { Column, Task } from '@/types'; import TaskItem from './TaskItem.vue'; +import ColumnDeleteDialog from './ColumnDeleteDialog.vue'; import { Button } from '@/components/ui/button'; import { Pencil, Trash2 } from 'lucide-vue-next'; import { router } from '@inertiajs/vue3'; @@ -39,17 +40,18 @@ const saveColumnName = () => { } }; +const isDeleteColumnOpen = ref(false); + const deleteColumn = () => { - if (confirm('Are you sure you want to delete this column?')) { - router.delete(`/columns/${props.column.id}`, { - preserveScroll: true, - onSuccess: () => toast.success('Column deleted'), - onError: (err) => { - if (err.message) toast.error(err.message); - else toast.error('Failed to delete column'); - } - }); - } + isDeleteColumnOpen.value = false; + router.delete(`/columns/${props.column.id}`, { + preserveScroll: true, + onSuccess: () => toast.success('Column deleted'), + onError: (err) => { + if (err.column) toast.error(err.column); + else toast.error('Failed to delete column'); + } + }); }; const onDragChange = (event: any) => { @@ -88,7 +90,7 @@ const onDragChange = (event: any) => { - @@ -110,6 +112,12 @@ const onDragChange = (event: any) => { + + diff --git a/resources/js/components/tasks/TaskCreateDialog.vue b/resources/js/components/tasks/TaskCreateDialog.vue index 0020956..ae05b68 100644 --- a/resources/js/components/tasks/TaskCreateDialog.vue +++ b/resources/js/components/tasks/TaskCreateDialog.vue @@ -24,6 +24,7 @@ const isOpen = ref(false); const form = useForm({ title: '', + description: '', due_date: '', }); @@ -67,6 +68,16 @@ const submit = () => { /> +
+ + + +
import InputError from '@/components/InputError.vue'; import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, @@ -28,8 +27,8 @@ const isOpen = defineModel('open', { default: false }); const form = useForm({ title: '', + description: '', due_date: '', - completed: false, }); const submit = () => { @@ -47,8 +46,8 @@ const submit = () => { watch([() => props.task, isOpen], ([task, open]) => { if (task && open) { form.title = task.title; - form.due_date = new Date(task.due_date).toISOString().slice(0, 16); - form.completed = Boolean(task.completed); + form.description = task.description || ''; + form.due_date = task.due_date ? new Date(task.due_date).toISOString().slice(0, 16) : ''; form.clearErrors(); } }); @@ -58,7 +57,7 @@ watch([() => props.task, isOpen], ([task, open]) => { - Edit task + Task details Update the task details @@ -74,6 +73,17 @@ watch([() => props.task, isOpen], ([task, open]) => {
+
+ + + +
+
props.task, isOpen], ([task, open]) => {
-
- - -
-