diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index a7618c0..8d31622 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -4,6 +4,8 @@ namespace App\Http\Controllers; use App\Models\Project; use Illuminate\Http\Request; +use App\Models\User; + class ProjectController extends Controller { @@ -12,8 +14,17 @@ class ProjectController extends Controller */ public function index() { + $user = User::find(auth()->user()->id); + $projects = $user->projects; + // Aggregate all todos for all projects + $todos = $projects->map(function ($project) { + return $project->todos; + })->flatten(); + return view('project.index', [ - 'projects' => Project::where('user_id', auth()->user()->id)->get() + 'projects' => $projects, + 'todos' => $todos->whereNull('completed_at')->values(), + 'completed' => $todos->whereNotNull('completed_at')->values(), ]); } @@ -22,11 +33,12 @@ class ProjectController extends Controller */ public function create() { - // + return view('project.create'); } /** - * Store a newly created resource in storage. + * TODO: Complete this method + * Store a newly created project in storage. */ public function store(Request $request) { @@ -34,6 +46,7 @@ class ProjectController extends Controller } /** + * TODO: Complete this method (if needed) * Display the specified resource. */ public function show(Project $project) @@ -42,7 +55,8 @@ class ProjectController extends Controller } /** - * Show the form for editing the specified resource. + * TODO: Complete this method (if needed) + * Show the form for editing the specified Project. */ public function edit(Project $project) { @@ -50,7 +64,7 @@ class ProjectController extends Controller } /** - * Update the specified resource in storage. + * Update the specified Project in storage. */ public function update(Request $request, Project $project) { @@ -58,10 +72,16 @@ class ProjectController extends Controller } /** - * Remove the specified resource from storage. + * Remove the specified Project from storage. */ public function destroy(Project $project) { - // + $user = User::find(auth()->user()->id); + $projects = $user->projects; + $project = $projects->find($project->id); + + $project->delete(); + + return redirect()->route('project.index'); } } diff --git a/app/Http/Controllers/ProjectTodoController.php b/app/Http/Controllers/ProjectTodoController.php new file mode 100644 index 0000000..c106068 --- /dev/null +++ b/app/Http/Controllers/ProjectTodoController.php @@ -0,0 +1,157 @@ +user()->id); + $projects = $user->projects; + $project = $projects->find($project_id); + + $todos = $project->todos; + + return view('project.todo', [ + 'todos' => $todos->whereNull('completed_at')->values(), + 'completed' => $todos->whereNotNull('completed_at')->values(), + 'project' => $project, + ]); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): Factory|View|Application + { + return view('project.create'); + } + + /** + * Store a newly created Todo in storage. + */ + public function store($project_id, Request $request) + { + $validatedData = Request::validate([ + 'title' => 'required|max:255', + 'description' => 'nullable|max:255', + 'due_start' => 'nullable|date', + // due_end is not required, but if it is provided, it must be after due_start + 'due_end' => 'nullable|after:due_start', + ]); + + $due_end = match (true) { + isset($validatedData['due_end']) => strtotime($validatedData['due_end']), + isset($validatedData['due_start']) => strtotime($validatedData['due_start']), + default => null, + }; + + $validatedData = array_merge($validatedData, [ + // due_end = due_start if due_end is not provided and due_start is provided or null + 'due_end' => $due_end, + 'user_id' => auth()->user()->id, + ]); + + // Modify all dates to unix timestamp + if (isset($validatedData['due_start'])) + $validatedData['due_start'] = strtotime($validatedData['due_start']); + if (isset($validatedData['due_end'])) + $validatedData['due_end'] = strtotime($validatedData['due_end']); + + $todo = new Todo($validatedData); + + $user = User::find(auth()->user()->id); + $project = $user->projects->find($project_id); + $project->todos()->save($todo); + + return redirect()->route('project.todo.index', $project_id) + ->with('success', 'Todo created successfully.'); + } + + /** + * Display the specified resource. + */ + public function show($project_id, Todo $todo) + { + $user = User::find(auth()->user()->id); + $projects = $user->projects; + $project = $projects->find($project_id); + + return view('project.todo.show', compact('project', 'todo')); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit($project_id, Todo $todo) + { + $user = User::find(auth()->user()->id); + $projects = $user->projects; + $project = $projects->find($project_id); + + return view('project.edit', compact('project', 'todo')); + } + + /** + * Update the specified resource in storage. + */ + public function update($project_id, Request $request, Todo $todo) + { + $data = Request::only(['title', 'description', 'due_start', 'due_end', 'completed_at']); + + if (Request::filled('completed_at')) { + $todo->completed_at = Request::input('completed_at') === 'on' ? strtotime(now($this->timezone)) : null; + $todo->save(); + return back()->with('success', 'Todo updated successfully'); + } else { + // If 'completed_at' is not provided, toggle its value (only if the request is empty) + if (empty($data)) + $todo->completed_at = $todo->completed_at ? null : strtotime(now($this->timezone)); + else + // Continue to update other fields + unset($data['completed_at']); + } + + if (Request::filled('due_start')) { + $data['due_start'] = strtotime(Request::input('due_start')); + } + + if (Request::filled('due_end')) { + $data['due_end'] = strtotime(Request::input('due_end')); + } elseif (isset($data['due_start'])) { + // If 'due_end' is not provided, set it to 'due_start' value + $data['due_end'] = strtotime(Request::input('due_start')); + } + + $todo->update($data); + + return redirect()->route('project.todo.index', $project_id) + ->with('success', 'Todo updated successfully'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy($project_id, Todo $todo) + { + $todo->delete(); + + return redirect()->route('project.todo.index', $project_id) + ->with('success', 'Todo deleted successfully'); + } +} diff --git a/app/Http/Controllers/TodoController.php b/app/Http/Controllers/TodoController.php index 4a3e8a7..ecc6025 100644 --- a/app/Http/Controllers/TodoController.php +++ b/app/Http/Controllers/TodoController.php @@ -26,13 +26,13 @@ class TodoController extends Controller $projects = $user->projects; - $allTodos = []; - $allProjects = []; + $todos = collect(); foreach ($projects as $project) { - $todos = $project->todos; + $todos = $todos->merge($project->todos); } + return view('todo.index', [ 'todos' => $todos->whereNull('completed_at')->values(), 'completed' => $todos->whereNotNull('completed_at')->values(), @@ -81,10 +81,9 @@ class TodoController extends Controller $todo = new Todo($validatedData); - $user = User::find(auth()->user()->id); $project = $user->projects->first(); - $project->todo()->save($todo); + $project->todos()->save($todo); // Set flash message session()->flash('success', 'Todo created successfully.'); @@ -97,9 +96,13 @@ class TodoController extends Controller */ public function show(Todo $todo): Factory|View|Application { - $todo = Todo::where('user_id', auth()->user()->id) + $user = User::find(auth()->user()->id); + $projects = $user->projects; + $todo = $projects->first() + ->todos ->where('id', $todo->id) - ->firstOrFail(); + ->first(); + return view('todo.show', compact('todo')); } @@ -108,9 +111,13 @@ class TodoController extends Controller */ public function edit(Todo $todo): Factory|View|Application { - $todo = Todo::where('user_id', auth()->user()->id) + $user = User::find(auth()->user()->id); + $projects = $user->projects; + $todo = $projects->first() + ->todos ->where('id', $todo->id) - ->firstOrFail(); + ->first(); + return view('todo.edit', compact('todo')); } @@ -147,7 +154,7 @@ class TodoController extends Controller $data['due_end'] = strtotime(Request::input('due_end')); } elseif (isset($data['due_start'])) { // If 'due_end' is not provided, set it to 'due_start' value - $data['due_end'] = $data['due_start']; + $data['due_end'] = strtotime(Request::input('due_start')); } $todo->update($data); diff --git a/app/Models/Project.php b/app/Models/Project.php index d3ec672..365e7ed 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; class Project extends Model { @@ -21,12 +22,12 @@ class Project extends Model ]; /** - * Relationship with Todo model + * Relationship with Todo model (one to many) * @return BelongsToMany */ - public function todos(): belongsToMany + public function todos(): HasManyThrough { - return $this->belongsToMany(Todo::class); + return $this->hasManyThrough(Todo::class, projectTodo::class, 'project_id', 'id', 'id', 'todo_id'); } public function user(): BelongsTo diff --git a/app/Models/Todo.php b/app/Models/Todo.php index 6cb7a4d..66c84cb 100644 --- a/app/Models/Todo.php +++ b/app/Models/Todo.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Traits\UuidTrait; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Database\Eloquent\Relations\MorphTo; class Todo extends Model @@ -36,9 +37,9 @@ class Todo extends Model return $this->belongsTo(User::class); } - public function project(): BelongsTo + public function project(): HasOneThrough { - return $this->belongsTo(projectTodo::class, 'project_todo', 'todo_id', 'project_id'); + return $this->hasOneThrough(Project::class, ProjectTodo::class, 'todo_id', 'id', 'id', 'project_id'); } } diff --git a/app/Models/User.php b/app/Models/User.php index 6f667b1..e73d05a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,8 +5,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use App\Traits\UuidTrait; use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; -use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -46,8 +45,13 @@ class User extends Authenticatable 'password' => 'hashed', ]; - public function projects(): belongsToMany + public function projects(): HasManyThrough { - return $this->belongsToMany(Project::class); + return $this->hasManyThrough(Project::class, projectUser::class, 'user_id', 'id', 'id', 'project_id'); + } + + public function todos(): HasManyThrough + { + return $this->hasManyThrough(Todo::class, projectUser::class, 'user_id', 'id', 'id', 'project_id'); } } diff --git a/app/Models/projectTodo.php b/app/Models/projectTodo.php new file mode 100644 index 0000000..608309d --- /dev/null +++ b/app/Models/projectTodo.php @@ -0,0 +1,29 @@ +belongsTo(Project::class); + } + + public function todo(): BelongsTo + { + return $this->belongsTo(Todo::class); + } +} diff --git a/app/Models/projectUser.php b/app/Models/projectUser.php new file mode 100644 index 0000000..14ce565 --- /dev/null +++ b/app/Models/projectUser.php @@ -0,0 +1,29 @@ +belongsTo(Project::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php new file mode 100644 index 0000000..9a45928 --- /dev/null +++ b/database/factories/ProjectFactory.php @@ -0,0 +1,24 @@ + + */ +class ProjectFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->sentence(3), + 'description' => $this->faker->sentence(10), + ]; + } +} diff --git a/database/factories/ProjectTodoFactory.php b/database/factories/ProjectTodoFactory.php new file mode 100644 index 0000000..7bb3903 --- /dev/null +++ b/database/factories/ProjectTodoFactory.php @@ -0,0 +1,24 @@ + + */ +class ProjectTodoFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'project_id' => $this->faker->uuid, + 'todo_id' => $this->faker->uuid, + ]; + } +} diff --git a/database/factories/ProjectUserFactory.php b/database/factories/ProjectUserFactory.php new file mode 100644 index 0000000..635407e --- /dev/null +++ b/database/factories/ProjectUserFactory.php @@ -0,0 +1,24 @@ + + */ +class ProjectUserFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'project_id' => $this->faker->uuid, + 'user_id' => $this->faker->uuid, + ]; + } +} diff --git a/database/factories/TodoFactory.php b/database/factories/TodoFactory.php index b58561b..9ff302a 100644 --- a/database/factories/TodoFactory.php +++ b/database/factories/TodoFactory.php @@ -21,11 +21,9 @@ class TodoFactory extends Factory 'description' => fake()->paragraph(), 'due_start' => fake()->unixTime(), 'due_end' => fake()->unixTime(), - 'user_id' => fake()->uuid(), - 'completed_at' => null, + 'completed_at' => fake()->optional()->unixTime(), 'created_at' => fake()->unixTime(), 'updated_at' => fake()->unixTime(), - ]; } } diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index f09151e..becf087 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -19,8 +19,8 @@ @@ -45,8 +45,8 @@ {{ __('Profile') }} - - {{ __('Todo List') }} + + {{ __('Projects') }} @@ -82,8 +82,8 @@ {{ __('Dashboard') }} - - {{ __('Todo List') }} + + {{ __('Projects') }} diff --git a/resources/views/project/edit.blade.php b/resources/views/project/edit.blade.php new file mode 100644 index 0000000..b031a70 --- /dev/null +++ b/resources/views/project/edit.blade.php @@ -0,0 +1,120 @@ + + +

+ {{ __('Edit Todo') }} +

+
+ +
+
+ @csrf + @method('PUT') +
+
+
+ +
+ + + @error('title') +
+ {{ $message }} +
+ @enderror +
+ +
+ + + @error('description') +
+ {{ $message }} +
+ @enderror +
+ +
+
+ + + @error('due_start') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('due_end') +
+ {{ $message }} +
+ @enderror +
+
+ +
+ + + + Cancel + + + + + + + +
+ + +
+ + + + + + + + diff --git a/resources/views/project/index.blade.php b/resources/views/project/index.blade.php new file mode 100644 index 0000000..8bfff72 --- /dev/null +++ b/resources/views/project/index.blade.php @@ -0,0 +1,189 @@ + + +

+ {{ __('Project List') }} + + Create Project + +

+
+ + +
+
+
+ @foreach($projects as $project) +
+ +
+
+
+

{{ $project->name }}

+

{{ $project->description }}

+
+
+
+
+
+ @csrf + @method('DELETE') + + +
+
+ @endforeach +
+
+
+ + + + + + + +
+

+ Completed Today + ({{ $completed->count() }}) +

+ +
+ +
diff --git a/resources/views/project/todo.blade.php b/resources/views/project/todo.blade.php new file mode 100644 index 0000000..d1c0ccf --- /dev/null +++ b/resources/views/project/todo.blade.php @@ -0,0 +1,155 @@ + + + +

+ {{ __('Todo List for Project:') }} {{ $project->name }} +

+
+ + + @if(session()->has('success')) +
+ {{ session('success') }} +
+ @elseif(session()->has('error')) +
+ {{ session('error') }} +
+ @endif + +
+
+ @csrf +
+
+ + + + +
+ @error('title') +
+ {{ $message }} +
+ @enderror +
+
+
+ + + + + +
+

+ Completed Today + ({{ $completed->count() }}) +

+ +
+
diff --git a/resources/views/todo/edit.blade.php b/resources/views/todo/edit.blade.php index 2a876bd..cb49564 100644 --- a/resources/views/todo/edit.blade.php +++ b/resources/views/todo/edit.blade.php @@ -41,7 +41,7 @@ + value="{{ old('due_start', $todo->due_start ? date('Y-m-d\TH:i', $todo->due_start) : '') }}"> @error('due_start')
{{ $message }} diff --git a/routes/web.php b/routes/web.php index 8f721b3..c65c3a7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,8 +1,9 @@ middleware(['auth', 'verified']) ->name('dashboard'); -// Show dashboard after login, get user data from TodoController -//Route::get('/dashboard', [TodoController::class, 'index']) -// ->middleware(['auth', 'verified']) -// ->name('dashboard'); - -// todo resource route -Route::resource('todo', TodoController::class) +Route::resource('project.todo', ProjectTodoController::class) ->middleware([ 'auth', 'verified', 'web' ]); +Route::resource('project', ProjectController::class) + ->middleware([ + 'auth', + 'verified', + 'web' + ]); Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');