Commit the Projects/Tasks/Ideas/TaskComments feature

This was already implemented, migrated, and running live, but had
never been committed. Bringing it into git: Project/Task/Idea/
TaskComment models, their JSON API (CRUD + status transitions,
task comments, idea<->project linking), Livewire UI pages, and the
nav/route wiring for them. All 42 of its own tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
m105
2026-08-06 20:41:57 +03:00
co-authored by Claude Sonnet 5
parent 6ff7f78c9c
commit a4ff6c0ccb
39 changed files with 3411 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
<?php
namespace App\Livewire\Tasks;
use App\Models\Project;
use App\Models\Task;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('components.layouts.app')]
class Index extends Component
{
public bool $showForm = false;
public ?int $editingId = null;
public string $title = '';
public ?string $description = null;
public string $status = 'todo';
public ?int $projectId = null;
public int $priority = 0;
public ?string $dueAt = null;
public ?string $estimatedHours = null;
public ?string $actualHours = null;
public string $search = '';
public string $statusFilter = '';
public string $priorityFilter = '';
public ?int $projectFilter = null;
public function mount()
{
// If project_id is passed as a query parameter, filter by it
if (request()->has('project_id')) {
$this->projectFilter = (int) request('project_id');
}
}
public function render()
{
$query = Task::with(['project', 'comments']);
if ($this->search) {
$query->where('title', 'like', '%' . $this->search . '%')
->orWhere('description', 'like', '%' . $this->search . '%');
}
if ($this->statusFilter) {
$query->where('status', $this->statusFilter);
}
if ($this->priorityFilter) {
$query->where('priority', $this->priorityFilter);
}
if ($this->projectFilter) {
$query->where('project_id', $this->projectFilter);
}
$tasks = $query->orderBy('priority', 'desc')->orderBy('created_at')->get();
return view('livewire.tasks.index', [
'tasks' => $tasks,
'projects' => Project::orderBy('name')->get(),
'statuses' => Task::STATUSES,
'totalTasks' => Task::count(),
'todoTasks' => Task::todo()->count(),
'inProgressTasks' => Task::inProgress()->count(),
'doneTasks' => Task::done()->count(),
'blockedTasks' => Task::blocked()->count(),
'waitingReviewTasks' => Task::waitingReview()->count(),
'dueSoonTasks' => Task::dueSoon()->count(),
'overdueTasks' => Task::overdue()->count(),
]);
}
public function create(): void
{
$this->resetForm();
// Pre-select the filtered project if one is selected
if ($this->projectFilter) {
$this->projectId = $this->projectFilter;
}
$this->showForm = true;
}
public function edit(int $id): void
{
$task = Task::findOrFail($id);
$this->editingId = $task->id;
$this->title = $task->title;
$this->description = $task->description;
$this->status = $task->status;
$this->projectId = $task->project_id;
$this->priority = $task->priority;
$this->dueAt = $task->due_at?->format('Y-m-d\TH:i') ?? '';
$this->estimatedHours = $task->estimated_hours;
$this->actualHours = $task->actual_hours;
$this->showForm = true;
}
public function save(): void
{
$validated = $this->validate([
'title' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'status' => ['required', 'string', 'in:' . implode(',', Task::STATUSES)],
'projectId' => ['nullable', 'integer', 'exists:projects,id'],
'priority' => ['integer', 'min:0', 'max:10'],
'dueAt' => ['nullable', 'date'],
'estimatedHours' => ['nullable', 'numeric', 'min:0'],
'actualHours' => ['nullable', 'numeric', 'min:0'],
]);
Task::updateOrCreate(['id' => $this->editingId], [
'title' => $validated['title'],
'description' => $validated['description'],
'status' => $validated['status'],
'project_id' => $validated['projectId'],
'priority' => $validated['priority'],
'due_at' => $validated['dueAt'] ?: null,
'estimated_hours' => $validated['estimatedHours'] ?: null,
'actual_hours' => $validated['actualHours'] ?: null,
]);
$this->resetForm();
$this->showForm = false;
}
public function delete(int $id): void
{
Task::findOrFail($id)->delete();
}
public function changeStatus(int $id, string $newStatus): void
{
if (!in_array($newStatus, Task::STATUSES)) {
return;
}
$task = Task::findOrFail($id);
$task->update(['status' => $newStatus]);
// Auto-set timestamps based on status
if ($newStatus === 'in_progress' && !$task->started_at) {
$task->update(['started_at' => now()]);
}
if ($newStatus === 'done' && !$task->completed_at) {
$task->update(['completed_at' => now()]);
}
if ($newStatus !== 'in_progress' && $newStatus !== 'done') {
$task->update(['started_at' => null, 'completed_at' => null]);
}
}
public function clearFilters(): void
{
$this->search = '';
$this->statusFilter = '';
$this->priorityFilter = '';
$this->projectFilter = null;
}
public function resetForm(): void
{
$this->editingId = null;
$this->title = '';
$this->description = null;
$this->status = 'todo';
$this->projectId = null;
$this->priority = 0;
$this->dueAt = null;
$this->estimatedHours = null;
$this->actualHours = null;
$this->resetValidation();
}
public function getStatusColor(string $status): string
{
return match ($status) {
'todo' => 'bg-gray-100 text-gray-800',
'in_progress' => 'bg-blue-100 text-blue-800',
'done' => 'bg-green-100 text-green-800',
'blocked' => 'bg-red-100 text-red-800',
'waiting_review' => 'bg-yellow-100 text-yellow-800',
default => 'bg-gray-100 text-gray-800',
};
}
public function getPriorityLabel(int $priority): string
{
if ($priority === 0) return 'None';
if ($priority <= 3) return 'Low';
if ($priority <= 6) return 'Medium';
if ($priority <= 8) return 'High';
return 'Critical';
}
public function getCommentsCount(int $taskId): int
{
return \App\Models\TaskComment::where('task_id', $taskId)->count();
}
public function startTracking(int $id): void
{
$task = Task::findOrFail($id);
if ($task->status !== 'in_progress') {
$task->update(['status' => 'in_progress', 'started_at' => now()]);
}
}
public function stopTracking(int $id): void
{
// This would need more complex time tracking logic
// For now, just mark as done
$task = Task::findOrFail($id);
$task->update(['status' => 'done', 'completed_at' => now()]);
}
}