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:
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreIdeaRequest;
|
||||
use App\Http\Requests\UpdateIdeaRequest;
|
||||
use App\Http\Resources\IdeaResource;
|
||||
use App\Models\Idea;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class IdeaController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Idea::query();
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->string('status'));
|
||||
}
|
||||
|
||||
if ($request->filled('category')) {
|
||||
$query->where('category', $request->string('category'));
|
||||
}
|
||||
|
||||
if ($request->filled('project_id')) {
|
||||
$query->where('project_id', $request->integer('project_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$query->search($request->string('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('unlinked')) {
|
||||
$query->unlinked();
|
||||
}
|
||||
|
||||
if ($request->filled('priority')) {
|
||||
$query->where('priority', $request->integer('priority'));
|
||||
}
|
||||
|
||||
$ideas = $query->orderBy('priority', 'desc')->orderBy('created_at', 'desc')->get();
|
||||
|
||||
// Eager load relationships if requested
|
||||
if ($request->boolean('with_project')) {
|
||||
$ideas->load(['project']);
|
||||
}
|
||||
|
||||
if ($request->boolean('with_user')) {
|
||||
$ideas->load(['user']);
|
||||
}
|
||||
|
||||
return IdeaResource::collection($ideas);
|
||||
}
|
||||
|
||||
public function store(StoreIdeaRequest $request)
|
||||
{
|
||||
$idea = Idea::create($request->validated());
|
||||
|
||||
return (new IdeaResource($idea))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Request $request, Idea $idea)
|
||||
{
|
||||
if ($request->boolean('with_project')) {
|
||||
$idea->load(['project']);
|
||||
}
|
||||
|
||||
if ($request->boolean('with_user')) {
|
||||
$idea->load(['user']);
|
||||
}
|
||||
|
||||
return new IdeaResource($idea);
|
||||
}
|
||||
|
||||
public function update(UpdateIdeaRequest $request, Idea $idea)
|
||||
{
|
||||
$idea->update($request->validated());
|
||||
|
||||
return new IdeaResource($idea);
|
||||
}
|
||||
|
||||
public function destroy(Idea $idea)
|
||||
{
|
||||
$idea->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change idea status.
|
||||
*/
|
||||
public function changeStatus(Request $request, Idea $idea)
|
||||
{
|
||||
$request->validate([
|
||||
'status' => ['required', 'string', 'in:' . implode(',', Idea::STATUSES)],
|
||||
]);
|
||||
|
||||
$idea->update(['status' => $request->string('status')]);
|
||||
|
||||
return new IdeaResource($idea);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link an idea to a project.
|
||||
*/
|
||||
public function linkToProject(Request $request, Idea $idea)
|
||||
{
|
||||
$request->validate([
|
||||
'project_id' => ['required', 'integer', 'exists:projects,id'],
|
||||
]);
|
||||
|
||||
$idea->update(['project_id' => $request->integer('project_id')]);
|
||||
|
||||
return new IdeaResource($idea->load(['project']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink an idea from a project.
|
||||
*/
|
||||
public function unlinkFromProject(Idea $idea)
|
||||
{
|
||||
$idea->update(['project_id' => null]);
|
||||
|
||||
return new IdeaResource($idea);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreProjectRequest;
|
||||
use App\Http\Requests\UpdateProjectRequest;
|
||||
use App\Http\Resources\IdeaResource;
|
||||
use App\Http\Resources\ProjectResource;
|
||||
use App\Http\Resources\TaskResource;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProjectController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Project::query();
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->string('status'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$query->where('name', 'like', '%' . $request->string('name') . '%');
|
||||
}
|
||||
|
||||
$projects = $query->orderBy('name')->get();
|
||||
|
||||
// Eager load tasks if ?with_tasks=1
|
||||
if ($request->boolean('with_tasks')) {
|
||||
$projects->load(['tasks.comments']);
|
||||
}
|
||||
|
||||
return ProjectResource::collection($projects);
|
||||
}
|
||||
|
||||
public function store(StoreProjectRequest $request)
|
||||
{
|
||||
$project = Project::create($request->validated());
|
||||
|
||||
return (new ProjectResource($project))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Request $request, Project $project)
|
||||
{
|
||||
if ($request->boolean('with_tasks')) {
|
||||
$project->load(['tasks.comments']);
|
||||
}
|
||||
|
||||
return new ProjectResource($project);
|
||||
}
|
||||
|
||||
public function update(UpdateProjectRequest $request, Project $project)
|
||||
{
|
||||
$project->update($request->validated());
|
||||
|
||||
return new ProjectResource($project);
|
||||
}
|
||||
|
||||
public function destroy(Project $project)
|
||||
{
|
||||
$project->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks for a project.
|
||||
*/
|
||||
public function tasks(Request $request, Project $project)
|
||||
{
|
||||
$query = $project->tasks()->with(['comments']);
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->string('status'));
|
||||
}
|
||||
|
||||
if ($request->filled('priority')) {
|
||||
$query->where('priority', $request->integer('priority'));
|
||||
}
|
||||
|
||||
$tasks = $query->orderBy('priority', 'desc')->orderBy('created_at')->get();
|
||||
|
||||
return TaskResource::collection($tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ideas for a project.
|
||||
*/
|
||||
public function ideas(Request $request, Project $project)
|
||||
{
|
||||
$query = $project->ideas()->with(['user']);
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->string('status'));
|
||||
}
|
||||
|
||||
if ($request->filled('category')) {
|
||||
$query->where('category', $request->string('category'));
|
||||
}
|
||||
|
||||
if ($request->filled('priority')) {
|
||||
$query->where('priority', $request->integer('priority'));
|
||||
}
|
||||
|
||||
$ideas = $query->orderBy('priority', 'desc')->orderBy('created_at', 'desc')->get();
|
||||
|
||||
return IdeaResource::collection($ideas);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreTaskCommentRequest;
|
||||
use App\Http\Resources\TaskCommentResource;
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskComment;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TaskCommentController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get all comments for a task.
|
||||
*/
|
||||
public function index(Request $request, Task $task)
|
||||
{
|
||||
$comments = $task->comments()->orderBy('created_at')->get();
|
||||
|
||||
return TaskCommentResource::collection($comments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new comment for a task.
|
||||
*/
|
||||
public function store(StoreTaskCommentRequest $request, Task $task)
|
||||
{
|
||||
$comment = $task->comments()->create($request->validated());
|
||||
|
||||
return (new TaskCommentResource($comment))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a comment.
|
||||
*/
|
||||
public function update(Request $request, TaskComment $comment)
|
||||
{
|
||||
$request->validate([
|
||||
'body' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$comment->update([
|
||||
'body' => $request->string('body'),
|
||||
]);
|
||||
|
||||
return new TaskCommentResource($comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a single comment.
|
||||
*/
|
||||
public function show(TaskComment $comment)
|
||||
{
|
||||
return new TaskCommentResource($comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a comment.
|
||||
*/
|
||||
public function destroy(TaskComment $comment)
|
||||
{
|
||||
$comment->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreTaskRequest;
|
||||
use App\Http\Requests\UpdateTaskRequest;
|
||||
use App\Http\Resources\TaskResource;
|
||||
use App\Models\Task;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TaskController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Task::with(['project', 'comments']);
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->string('status'));
|
||||
}
|
||||
|
||||
if ($request->filled('project_id')) {
|
||||
$query->where('project_id', $request->integer('project_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('priority')) {
|
||||
$query->where('priority', $request->integer('priority'));
|
||||
}
|
||||
|
||||
if ($request->boolean('due_soon')) {
|
||||
$query->dueSoon($request->integer('days', 3));
|
||||
}
|
||||
|
||||
if ($request->boolean('overdue')) {
|
||||
$query->overdue();
|
||||
}
|
||||
|
||||
$tasks = $query->orderBy('priority', 'desc')->orderBy('created_at')->get();
|
||||
|
||||
return TaskResource::collection($tasks);
|
||||
}
|
||||
|
||||
public function store(StoreTaskRequest $request)
|
||||
{
|
||||
$task = Task::create($request->validated());
|
||||
|
||||
return (new TaskResource($task))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Request $request, Task $task)
|
||||
{
|
||||
$task->load(['project', 'comments']);
|
||||
|
||||
return new TaskResource($task);
|
||||
}
|
||||
|
||||
public function update(UpdateTaskRequest $request, Task $task)
|
||||
{
|
||||
// Handle status transitions
|
||||
$validated = $request->validated();
|
||||
|
||||
// Auto-set timestamps based on status
|
||||
if (isset($validated['status'])) {
|
||||
if ($validated['status'] === 'in_progress' && ! $task->started_at) {
|
||||
$validated['started_at'] = now();
|
||||
}
|
||||
if (in_array($validated['status'], ['done', 'completed']) && ! $task->completed_at) {
|
||||
$validated['completed_at'] = now();
|
||||
}
|
||||
if ($validated['status'] !== 'in_progress') {
|
||||
$validated['started_at'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$task->update($validated);
|
||||
|
||||
return new TaskResource($task->fresh(['project', 'comments']));
|
||||
}
|
||||
|
||||
public function destroy(Task $task)
|
||||
{
|
||||
$task->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comments for a task.
|
||||
*/
|
||||
public function comments(Request $request, Task $task)
|
||||
{
|
||||
$comments = $task->comments()->orderBy('created_at')->get();
|
||||
|
||||
return TaskCommentResource::collection($comments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change task status.
|
||||
*/
|
||||
public function changeStatus(Request $request, Task $task)
|
||||
{
|
||||
$request->validate([
|
||||
'status' => ['required', 'string', 'in:' . implode(',', Task::STATUSES)],
|
||||
]);
|
||||
|
||||
$status = $request->string('status');
|
||||
|
||||
$updateData = ['status' => $status];
|
||||
|
||||
// Auto-set timestamps
|
||||
if ($status === 'in_progress' && ! $task->started_at) {
|
||||
$updateData['started_at'] = now();
|
||||
}
|
||||
if (in_array($status, ['done', 'completed']) && ! $task->completed_at) {
|
||||
$updateData['completed_at'] = now();
|
||||
}
|
||||
if ($status !== 'in_progress') {
|
||||
$updateData['started_at'] = null;
|
||||
}
|
||||
|
||||
$task->update($updateData);
|
||||
|
||||
return new TaskResource($task->fresh(['project', 'comments']));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Idea;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreIdeaRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'status' => ['required', 'string', Rule::in(Idea::STATUSES)],
|
||||
'project_id' => ['nullable', 'integer', 'exists:projects,id'],
|
||||
'priority' => ['integer', 'min:0', 'max:10'],
|
||||
'category' => ['nullable', 'string', Rule::in(Idea::CATEGORIES)],
|
||||
'tags' => ['nullable', 'string', 'max:255'],
|
||||
'user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Project;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreProjectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'status' => ['required', 'string', Rule::in(Project::STATUSES)],
|
||||
'due_at' => ['nullable', 'date'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\TaskComment;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreTaskCommentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'body' => ['required', 'string'],
|
||||
'author_type' => ['required', 'string', Rule::in(TaskComment::AUTHOR_TYPES)],
|
||||
'author_name' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Task;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreTaskRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'status' => ['required', 'string', Rule::in(Task::STATUSES)],
|
||||
'project_id' => ['required', 'integer', 'exists:projects,id'],
|
||||
'priority' => ['integer', 'min:0', 'max:10'],
|
||||
'due_at' => ['nullable', 'date'],
|
||||
'estimated_hours' => ['nullable', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Idea;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateIdeaRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['sometimes', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'status' => ['sometimes', 'string', Rule::in(Idea::STATUSES)],
|
||||
'project_id' => ['nullable', 'integer', 'exists:projects,id'],
|
||||
'priority' => ['integer', 'min:0', 'max:10'],
|
||||
'category' => ['nullable', 'string', Rule::in(Idea::CATEGORIES)],
|
||||
'tags' => ['nullable', 'string', 'max:255'],
|
||||
'user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Project;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateProjectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['sometimes', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'status' => ['sometimes', 'string', Rule::in(Project::STATUSES)],
|
||||
'due_at' => ['nullable', 'date'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Task;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateTaskRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['sometimes', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'status' => ['sometimes', 'string', Rule::in(Task::STATUSES)],
|
||||
'project_id' => ['sometimes', 'integer', 'exists:projects,id'],
|
||||
'priority' => ['sometimes', 'integer', 'min:0', 'max:10'],
|
||||
'due_at' => ['nullable', 'date'],
|
||||
'started_at' => ['nullable', 'date'],
|
||||
'completed_at' => ['nullable', 'date'],
|
||||
'estimated_hours' => ['nullable', 'integer', 'min:0'],
|
||||
'actual_hours' => ['nullable', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class IdeaResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'status' => $this->status,
|
||||
'priority' => $this->priority,
|
||||
'category' => $this->category,
|
||||
'tags' => $this->tags,
|
||||
'tags_array' => $this->tags_array,
|
||||
'project_id' => $this->project_id,
|
||||
'user_id' => $this->user_id,
|
||||
'project' => $this->whenLoaded('project', fn () => [
|
||||
'id' => $this->project->id,
|
||||
'name' => $this->project->name,
|
||||
]),
|
||||
'user' => $this->whenLoaded('user', fn () => [
|
||||
'id' => $this->user->id,
|
||||
'name' => $this->user->name,
|
||||
]),
|
||||
'created_at' => $this->created_at->toIso8601String(),
|
||||
'updated_at' => $this->updated_at->toIso8601String(),
|
||||
'deleted_at' => $this->deleted_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ProjectResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'status' => $this->status,
|
||||
'due_at' => $this->due_at?->toIso8601String(),
|
||||
'created_at' => $this->created_at->toIso8601String(),
|
||||
'updated_at' => $this->updated_at->toIso8601String(),
|
||||
'deleted_at' => $this->deleted_at?->toIso8601String(),
|
||||
'task_count' => $this->whenLoaded('tasks', fn () => $this->tasks->count()),
|
||||
'active_task_count' => $this->whenLoaded('tasks', fn () => $this->activeTasks->count()),
|
||||
'completed_task_count' => $this->whenLoaded('tasks', fn () => $this->completedTasks->count()),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class TaskCommentResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'body' => $this->body,
|
||||
'author_type' => $this->author_type,
|
||||
'author_name' => $this->author_name,
|
||||
'task_id' => $this->task_id,
|
||||
'created_at' => $this->created_at->toIso8601String(),
|
||||
'updated_at' => $this->updated_at->toIso8601String(),
|
||||
'deleted_at' => $this->deleted_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class TaskResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'status' => $this->status,
|
||||
'priority' => $this->priority,
|
||||
'due_at' => $this->due_at?->toIso8601String(),
|
||||
'started_at' => $this->started_at?->toIso8601String(),
|
||||
'completed_at' => $this->completed_at?->toIso8601String(),
|
||||
'estimated_hours' => $this->estimated_hours,
|
||||
'actual_hours' => $this->actual_hours,
|
||||
'project_id' => $this->project_id,
|
||||
'comment_count' => $this->whenLoaded('comments', fn () => $this->comments->count()),
|
||||
'project' => $this->whenLoaded('project', fn () => [
|
||||
'id' => $this->project->id,
|
||||
'name' => $this->project->name,
|
||||
]),
|
||||
'comments' => $this->whenLoaded('comments', fn () => TaskCommentResource::collection($this->comments)),
|
||||
'created_at' => $this->created_at->toIso8601String(),
|
||||
'updated_at' => $this->updated_at->toIso8601String(),
|
||||
'deleted_at' => $this->deleted_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Ideas;
|
||||
|
||||
use App\Models\Idea;
|
||||
use App\Models\Project;
|
||||
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 = 'new';
|
||||
|
||||
public ?int $projectId = null;
|
||||
|
||||
public int $priority = 0;
|
||||
|
||||
public ?string $category = null;
|
||||
|
||||
public ?string $tags = null;
|
||||
|
||||
public string $search = '';
|
||||
|
||||
public string $statusFilter = '';
|
||||
|
||||
public string $categoryFilter = '';
|
||||
|
||||
public ?int $projectFilter = null;
|
||||
|
||||
public bool $unlinkedFilter = false;
|
||||
|
||||
public function render()
|
||||
{
|
||||
$query = Idea::with(['project', 'user']);
|
||||
|
||||
if ($this->search) {
|
||||
$query->search($this->search);
|
||||
}
|
||||
|
||||
if ($this->statusFilter) {
|
||||
$query->where('status', $this->statusFilter);
|
||||
}
|
||||
|
||||
if ($this->categoryFilter) {
|
||||
$query->where('category', $this->categoryFilter);
|
||||
}
|
||||
|
||||
if ($this->projectFilter) {
|
||||
$query->where('project_id', $this->projectFilter);
|
||||
}
|
||||
|
||||
if ($this->unlinkedFilter) {
|
||||
$query->unlinked();
|
||||
}
|
||||
|
||||
$ideas = $query->orderBy('priority', 'desc')->orderBy('created_at', 'desc')->get();
|
||||
|
||||
return view('livewire.ideas.index', [
|
||||
'ideas' => $ideas,
|
||||
'projects' => Project::orderBy('name')->get(),
|
||||
'statuses' => Idea::STATUSES,
|
||||
'categories' => Idea::CATEGORIES,
|
||||
'totalIdeas' => Idea::count(),
|
||||
'newIdeas' => Idea::new()->count(),
|
||||
'consideredIdeas' => Idea::considered()->count(),
|
||||
'plannedIdeas' => Idea::planned()->count(),
|
||||
'implementedIdeas' => Idea::implemented()->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
|
||||
{
|
||||
$idea = Idea::findOrFail($id);
|
||||
$this->editingId = $idea->id;
|
||||
$this->title = $idea->title;
|
||||
$this->description = $idea->description;
|
||||
$this->status = $idea->status;
|
||||
$this->projectId = $idea->project_id;
|
||||
$this->priority = $idea->priority;
|
||||
$this->category = $idea->category;
|
||||
$this->tags = $idea->tags;
|
||||
$this->showForm = true;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$validated = $this->validate([
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'status' => ['required', 'string', 'in:' . implode(',', Idea::STATUSES)],
|
||||
'projectId' => ['nullable', 'integer', 'exists:projects,id'],
|
||||
'priority' => ['integer', 'min:0', 'max:10'],
|
||||
'category' => ['nullable', 'string', 'in:' . implode(',', Idea::CATEGORIES)],
|
||||
'tags' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
Idea::updateOrCreate(['id' => $this->editingId], [
|
||||
'title' => $validated['title'],
|
||||
'description' => $validated['description'],
|
||||
'status' => $validated['status'],
|
||||
'project_id' => $validated['projectId'],
|
||||
'priority' => $validated['priority'],
|
||||
'category' => $validated['category'],
|
||||
'tags' => $validated['tags'],
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
$this->resetForm();
|
||||
$this->showForm = false;
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
Idea::findOrFail($id)->delete();
|
||||
}
|
||||
|
||||
public function changeStatus(int $id, string $newStatus): void
|
||||
{
|
||||
if (!in_array($newStatus, Idea::STATUSES)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Idea::findOrFail($id)->update(['status' => $newStatus]);
|
||||
}
|
||||
|
||||
public function linkToProject(int $id, int $projectId): void
|
||||
{
|
||||
Idea::findOrFail($id)->update(['project_id' => $projectId]);
|
||||
}
|
||||
|
||||
public function unlinkFromProject(int $id): void
|
||||
{
|
||||
Idea::findOrFail($id)->update(['project_id' => null]);
|
||||
}
|
||||
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->statusFilter = '';
|
||||
$this->categoryFilter = '';
|
||||
$this->projectFilter = null;
|
||||
$this->unlinkedFilter = false;
|
||||
}
|
||||
|
||||
public function resetForm(): void
|
||||
{
|
||||
$this->editingId = null;
|
||||
$this->title = '';
|
||||
$this->description = null;
|
||||
$this->status = 'new';
|
||||
$this->projectId = null;
|
||||
$this->priority = 0;
|
||||
$this->category = null;
|
||||
$this->tags = null;
|
||||
$this->resetValidation();
|
||||
}
|
||||
|
||||
public function getStatusColor(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'new' => 'bg-blue-100 text-blue-800',
|
||||
'considered' => 'bg-yellow-100 text-yellow-800',
|
||||
'planned' => 'bg-purple-100 text-purple-800',
|
||||
'rejected' => 'bg-red-100 text-red-800',
|
||||
'implemented' => 'bg-green-100 text-green-800',
|
||||
default => 'bg-gray-100 text-gray-800',
|
||||
};
|
||||
}
|
||||
|
||||
public function getCategoryColor(string $category): string
|
||||
{
|
||||
return match ($category) {
|
||||
'feature' => 'bg-blue-100 text-blue-800',
|
||||
'improvement' => 'bg-purple-100 text-purple-800',
|
||||
'bugfix' => 'bg-red-100 text-red-800',
|
||||
'research' => 'bg-yellow-100 text-yellow-800',
|
||||
'other' => 'bg-gray-100 text-gray-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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Projects;
|
||||
|
||||
use App\Models\Project;
|
||||
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 $name = '';
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public string $status = 'active';
|
||||
|
||||
public ?string $dueAt = null;
|
||||
|
||||
public string $search = '';
|
||||
|
||||
public string $statusFilter = '';
|
||||
|
||||
public function render()
|
||||
{
|
||||
$query = Project::query();
|
||||
|
||||
if ($this->search) {
|
||||
$query->where('name', 'like', '%' . $this->search . '%')
|
||||
->orWhere('description', 'like', '%' . $this->search . '%');
|
||||
}
|
||||
|
||||
if ($this->statusFilter) {
|
||||
$query->where('status', $this->statusFilter);
|
||||
}
|
||||
|
||||
$projects = $query->orderBy('name')->get();
|
||||
|
||||
return view('livewire.projects.index', [
|
||||
'projects' => $projects,
|
||||
'statuses' => Project::STATUSES,
|
||||
'totalProjects' => Project::count(),
|
||||
'activeProjects' => Project::active()->count(),
|
||||
'completedProjects' => Project::completed()->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->resetForm();
|
||||
$this->showForm = true;
|
||||
}
|
||||
|
||||
public function edit(int $id): void
|
||||
{
|
||||
$project = Project::findOrFail($id);
|
||||
$this->editingId = $project->id;
|
||||
$this->name = $project->name;
|
||||
$this->description = $project->description;
|
||||
$this->status = $project->status;
|
||||
$this->dueAt = $project->due_at?->format('Y-m-d\TH:i') ?? '';
|
||||
$this->showForm = true;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'status' => ['required', 'string', 'in:' . implode(',', Project::STATUSES)],
|
||||
'dueAt' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
Project::updateOrCreate(['id' => $this->editingId], [
|
||||
'name' => $validated['name'],
|
||||
'description' => $validated['description'],
|
||||
'status' => $validated['status'],
|
||||
'due_at' => $validated['dueAt'] ?: null,
|
||||
]);
|
||||
|
||||
$this->resetForm();
|
||||
$this->showForm = false;
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
Project::findOrFail($id)->delete();
|
||||
}
|
||||
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->statusFilter = '';
|
||||
}
|
||||
|
||||
public function resetForm(): void
|
||||
{
|
||||
$this->editingId = null;
|
||||
$this->name = '';
|
||||
$this->description = null;
|
||||
$this->status = 'active';
|
||||
$this->dueAt = null;
|
||||
$this->resetValidation();
|
||||
}
|
||||
|
||||
public function getTasksCount(int $projectId): int
|
||||
{
|
||||
return \App\Models\Task::where('project_id', $projectId)->count();
|
||||
}
|
||||
|
||||
public function getStatusColor(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'active' => 'bg-green-100 text-green-800',
|
||||
'completed' => 'bg-blue-100 text-blue-800',
|
||||
'archived' => 'bg-gray-100 text-gray-800',
|
||||
'on_hold' => 'bg-yellow-100 text-yellow-800',
|
||||
'planned' => 'bg-purple-100 text-purple-800',
|
||||
default => 'bg-gray-100 text-gray-800',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\TaskComments;
|
||||
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskComment;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Layout('components.layouts.app')]
|
||||
class Index extends Component
|
||||
{
|
||||
public ?int $taskId = null;
|
||||
|
||||
public bool $showForm = false;
|
||||
|
||||
public ?int $editingId = null;
|
||||
|
||||
public string $body = '';
|
||||
|
||||
public string $authorType = 'user';
|
||||
|
||||
public string $authorName = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
// Get task_id from query parameters
|
||||
if (request()->has('task_id')) {
|
||||
$this->taskId = (int) request('task_id');
|
||||
}
|
||||
|
||||
// Set default author name for user
|
||||
if (auth()->check()) {
|
||||
$this->authorName = auth()->user()->name ?? 'User';
|
||||
} else {
|
||||
$this->authorName = 'User';
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$task = null;
|
||||
$comments = collect();
|
||||
|
||||
if ($this->taskId) {
|
||||
$task = Task::with(['project', 'comments'])->find($this->taskId);
|
||||
$comments = $task ? $task->comments()->orderBy('created_at')->get() : collect();
|
||||
}
|
||||
|
||||
return view('livewire.task-comments.index', [
|
||||
'task' => $task,
|
||||
'comments' => $comments,
|
||||
'authorTypes' => TaskComment::AUTHOR_TYPES,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->resetForm();
|
||||
$this->showForm = true;
|
||||
}
|
||||
|
||||
public function edit(int $id): void
|
||||
{
|
||||
$comment = TaskComment::findOrFail($id);
|
||||
$this->editingId = $comment->id;
|
||||
$this->body = $comment->body;
|
||||
$this->authorType = $comment->author_type;
|
||||
$this->authorName = $comment->author_name;
|
||||
$this->showForm = true;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
if (!$this->taskId) {
|
||||
$this->addError('taskId', 'Task ID is required');
|
||||
return;
|
||||
}
|
||||
|
||||
$validated = $this->validate([
|
||||
'body' => ['required', 'string'],
|
||||
'authorType' => ['required', 'string', 'in:' . implode(',', TaskComment::AUTHOR_TYPES)],
|
||||
'authorName' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
TaskComment::updateOrCreate(['id' => $this->editingId], [
|
||||
'task_id' => $this->taskId,
|
||||
'body' => $validated['body'],
|
||||
'author_type' => $validated['authorType'],
|
||||
'author_name' => $validated['authorName'],
|
||||
]);
|
||||
|
||||
$this->resetForm();
|
||||
$this->showForm = false;
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
TaskComment::findOrFail($id)->delete();
|
||||
}
|
||||
|
||||
public function resetForm(): void
|
||||
{
|
||||
$this->editingId = null;
|
||||
$this->body = '';
|
||||
$this->authorType = 'user';
|
||||
// Keep the author name as it was
|
||||
$this->resetValidation();
|
||||
}
|
||||
|
||||
public function getAuthorColor(string $authorType): string
|
||||
{
|
||||
return match ($authorType) {
|
||||
'ai' => 'bg-purple-100 text-purple-800',
|
||||
'system' => 'bg-gray-100 text-gray-800',
|
||||
'user' => 'bg-blue-100 text-blue-800',
|
||||
default => 'bg-gray-100 text-gray-800',
|
||||
};
|
||||
}
|
||||
|
||||
public function setAuthorAsAI(): void
|
||||
{
|
||||
$this->authorType = 'ai';
|
||||
$this->authorName = 'AI Assistant';
|
||||
}
|
||||
|
||||
public function setAuthorAsUser(): void
|
||||
{
|
||||
$this->authorType = 'user';
|
||||
if (auth()->check()) {
|
||||
$this->authorName = auth()->user()->name ?? 'User';
|
||||
} else {
|
||||
$this->authorName = 'User';
|
||||
}
|
||||
}
|
||||
|
||||
public function setAuthorAsSystem(): void
|
||||
{
|
||||
$this->authorType = 'system';
|
||||
$this->authorName = 'System';
|
||||
}
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Idea extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const STATUSES = ['new', 'considered', 'planned', 'rejected', 'implemented'];
|
||||
|
||||
public const CATEGORIES = ['feature', 'improvement', 'bugfix', 'research', 'other'];
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'description',
|
||||
'status',
|
||||
'project_id',
|
||||
'priority',
|
||||
'category',
|
||||
'tags',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'priority' => 'integer',
|
||||
];
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function scopeNew($query)
|
||||
{
|
||||
return $query->where('status', 'new');
|
||||
}
|
||||
|
||||
public function scopeConsidered($query)
|
||||
{
|
||||
return $query->where('status', 'considered');
|
||||
}
|
||||
|
||||
public function scopePlanned($query)
|
||||
{
|
||||
return $query->where('status', 'planned');
|
||||
}
|
||||
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('status', 'rejected');
|
||||
}
|
||||
|
||||
public function scopeImplemented($query)
|
||||
{
|
||||
return $query->where('status', 'implemented');
|
||||
}
|
||||
|
||||
public function scopeByCategory($query, string $category)
|
||||
{
|
||||
return $query->where('category', $category);
|
||||
}
|
||||
|
||||
public function scopeLinkedToProject($query)
|
||||
{
|
||||
return $query->whereNotNull('project_id');
|
||||
}
|
||||
|
||||
public function scopeUnlinked($query)
|
||||
{
|
||||
return $query->whereNull('project_id');
|
||||
}
|
||||
|
||||
public function scopeSearch($query, string $search)
|
||||
{
|
||||
return $query->where('title', 'like', '%' . $search . '%')
|
||||
->orWhere('description', 'like', '%' . $search . '%')
|
||||
->orWhere('tags', 'like', '%' . $search . '%');
|
||||
}
|
||||
|
||||
public function getTagsArrayAttribute(): array
|
||||
{
|
||||
return $this->tags ? explode(',', $this->tags) : [];
|
||||
}
|
||||
|
||||
public function setTagsAttribute(array|string|null $value): void
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$this->attributes['tags'] = implode(',', $value);
|
||||
} else {
|
||||
$this->attributes['tags'] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Project extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const STATUSES = ['active', 'completed', 'archived', 'on_hold', 'planned'];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'status',
|
||||
'due_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'due_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function tasks(): HasMany
|
||||
{
|
||||
return $this->hasMany(Task::class)->orderBy('priority', 'desc')->orderBy('created_at');
|
||||
}
|
||||
|
||||
public function ideas(): HasMany
|
||||
{
|
||||
return $this->hasMany(Idea::class)->orderBy('priority', 'desc')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function activeTasks()
|
||||
{
|
||||
return $this->tasks()->whereIn('status', ['todo', 'in_progress', 'waiting_review']);
|
||||
}
|
||||
|
||||
public function completedTasks()
|
||||
{
|
||||
return $this->tasks()->where('status', 'done');
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
public function scopeCompleted($query)
|
||||
{
|
||||
return $query->where('status', 'completed');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Task extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const STATUSES = ['todo', 'in_progress', 'done', 'blocked', 'waiting_review'];
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'description',
|
||||
'status',
|
||||
'project_id',
|
||||
'priority',
|
||||
'due_at',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'estimated_hours',
|
||||
'actual_hours',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'due_at' => 'datetime',
|
||||
'started_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function comments(): HasMany
|
||||
{
|
||||
return $this->hasMany(TaskComment::class)->orderBy('created_at');
|
||||
}
|
||||
|
||||
public function scopeByStatus($query, string $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
public function scopeTodo($query)
|
||||
{
|
||||
return $query->where('status', 'todo');
|
||||
}
|
||||
|
||||
public function scopeInProgress($query)
|
||||
{
|
||||
return $query->where('status', 'in_progress');
|
||||
}
|
||||
|
||||
public function scopeDone($query)
|
||||
{
|
||||
return $query->where('status', 'done');
|
||||
}
|
||||
|
||||
public function scopeBlocked($query)
|
||||
{
|
||||
return $query->where('status', 'blocked');
|
||||
}
|
||||
|
||||
public function scopeWaitingReview($query)
|
||||
{
|
||||
return $query->where('status', 'waiting_review');
|
||||
}
|
||||
|
||||
public function scopeDueSoon($query, int $days = 3)
|
||||
{
|
||||
return $query->where('due_at', '>=', now())
|
||||
->where('due_at', '<=', now()->addDays($days))
|
||||
->where('status', '!=', 'done');
|
||||
}
|
||||
|
||||
public function scopeOverdue($query)
|
||||
{
|
||||
return $query->where('due_at', '<', now())
|
||||
->where('status', '!=', 'done');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class TaskComment extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const AUTHOR_TYPES = ['user', 'ai', 'system'];
|
||||
|
||||
protected $fillable = [
|
||||
'task_id',
|
||||
'body',
|
||||
'author_type',
|
||||
'author_name',
|
||||
];
|
||||
|
||||
public function task(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Task::class);
|
||||
}
|
||||
|
||||
public function isFromAI(): bool
|
||||
{
|
||||
return $this->author_type === 'ai';
|
||||
}
|
||||
|
||||
public function isFromUser(): bool
|
||||
{
|
||||
return $this->author_type === 'user';
|
||||
}
|
||||
|
||||
public function isSystem(): bool
|
||||
{
|
||||
return $this->author_type === 'system';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user