mirror of https://github.com/Devoalda/LaDo.git
feature(Project):
Modified: - Relationships between tables (forward and backward) - TodoController is deprecated - Views for project index - Navigation for project instead of todo
This commit is contained in:
parent
f7d86460f6
commit
0bda828140
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
//use App\Http\Requests\StoreTodoRequest;
|
||||
//use App\Http\Requests\UpdateTodoRequest;
|
||||
use App\Models\Todo;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class ProjectTodoController extends Controller
|
||||
{
|
||||
private string $timezone = 'Asia/Singapore';
|
||||
/**
|
||||
* Display a listing of all Todos for a Project.
|
||||
*/
|
||||
public function index($project_id)
|
||||
{
|
||||
$user = User::find(auth()->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');
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class projectTodo extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'project_todo';
|
||||
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'todo_id',
|
||||
];
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function todo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Todo::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class projectUser extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'project_user';
|
||||
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Project>
|
||||
*/
|
||||
class ProjectFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->sentence(3),
|
||||
'description' => $this->faker->sentence(10),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Model>
|
||||
*/
|
||||
class ProjectTodoFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'project_id' => $this->faker->uuid,
|
||||
'todo_id' => $this->faker->uuid,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Model>
|
||||
*/
|
||||
class ProjectUserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'project_id' => $this->faker->uuid,
|
||||
'user_id' => $this->faker->uuid,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
|
||||
<!-- Todo List -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
|
||||
<x-nav-link :href="route('todo.index')" :active="request()->routeIs('todo.index')">
|
||||
{{ __('Todo List') }}
|
||||
<x-nav-link :href="route('project.index')" :active="request()->routeIs('project.index')">
|
||||
{{ __('Projects') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -45,8 +45,8 @@
|
|||
{{ __('Profile') }}
|
||||
</x-dropdown-link>
|
||||
|
||||
<x-dropdown-link :href="route('todo.index')">
|
||||
{{ __('Todo List') }}
|
||||
<x-dropdown-link :href="route('project.index')">
|
||||
{{ __('Projects') }}
|
||||
</x-dropdown-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
|
@ -82,8 +82,8 @@
|
|||
{{ __('Dashboard') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
<x-responsive-nav-link :href="route('todo.index')" :active="request()->routeIs('todo.index')">
|
||||
{{ __('Todo List') }}
|
||||
<x-responsive-nav-link :href="route('project.index')" :active="request()->routeIs('project.index')">
|
||||
{{ __('Projects') }}
|
||||
</x-responsive-nav-link>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Edit Todo') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-4">
|
||||
<form method="POST" action="{{ route('project.todo.update', [$project, $todo]) }}"
|
||||
id="todo-form">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
|
||||
<div class="text-gray-800 dark:text-gray-100">
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="title" class="block mb-2 font-semibold">Title</label>
|
||||
<input type="text" name="title" id="title" placeholder="Title"
|
||||
class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-blue-500 rounded-lg w-full p-4 @error('title') border-red-500 @enderror"
|
||||
value="{{ old('title', $todo->title) }}">
|
||||
@error('title')
|
||||
<div class="text-red-500 mt-2 text-sm">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="description" class="block mb-2 font-semibold">Description</label>
|
||||
<textarea name="description" id="description" placeholder="Description"
|
||||
class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-blue-500 rounded-lg w-full p-4 @error('description') border-red-500 @enderror">{{ old('description', $todo->description) }}</textarea>
|
||||
@error('description')
|
||||
<div class="text-red-500 mt-2 text-sm">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="due_start" class="block mb-2 font-semibold">Due Start</label>
|
||||
<input type="datetime-local" name="due_start" id="due_start" placeholder="Due Start"
|
||||
class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-blue-500 rounded-lg w-full p-4 @error('due_start') border-red-500 @enderror"
|
||||
value="{{ old('due_start', $todo->due_start ? date('Y-m-d\TH:i', $todo->due_start) : '') }}">
|
||||
@error('due_start')
|
||||
<div class="text-red-500 mt-2 text-sm">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="due_end" class="block mb-2 font-semibold">Due End</label>
|
||||
<input type="datetime-local" name="due_end" id="due_end" placeholder="Due End"
|
||||
class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-blue-500 rounded-lg w-full p-4 @error('due_end') border-red-500 @enderror"
|
||||
value="{{ old('due_end', $todo->due_end ? date('Y-m-d\TH:i', $todo->due_end) : '') }}">
|
||||
@error('due_end')
|
||||
<div class="text-red-900 mt-2 text-sm">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4 space-x-4">
|
||||
<!-- Cancel Button (GET request to index route) -->
|
||||
<a href="{{ route('project.todo.index', $project) }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-900 bg-transparent border border-gray-900 rounded-l-lg hover:bg-gray-900 hover:text-white focus:z-10 focus:ring-2 focus:ring-gray-500 focus:bg-gray-900 focus:text-white dark:border-white dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:bg-gray-700">
|
||||
<svg class="w-5 h-5 mr-2" aria-hidden="true" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<button type="button"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-red-600 rounded hover:bg-red-800 focus:z-10 focus:ring-2 focus:ring-red-500 border border-grey-900 dark:border-white dark:text-white dark:hover:text-white dark:hover:bg-red-700 dark:focus:bg-red-700"
|
||||
onclick="confirmDelete()">
|
||||
<svg class="w-5 h-5 mr-2" aria-hidden="true" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
<!-- Update Button -->
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-r-md hover:bg-green-800 focus:z-10 focus:ring-2 focus:ring-green-500 border border-grey-900 dark:border-white dark:text-white dark:hover:text-white dark:hover:bg-green-700 dark:focus:bg-green-700">
|
||||
<svg class="w-5 h-5 mr-2" aria-hidden="true" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Hidden Delete Form -->
|
||||
<form id="delete-form" action="{{ route('project.todo.destroy', [$project, $todo]) }}" method="POST"
|
||||
class="hidden">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function confirmDelete() {
|
||||
if (confirm('Are you sure you want to delete this item?')) {
|
||||
document.getElementById('delete-form').submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
</x-app-layout>
|
|
@ -0,0 +1,189 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Project List') }}
|
||||
<a href="{{ route('project.create') }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded float-right">
|
||||
Create Project
|
||||
</a>
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
|
||||
<div class="py-4">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach($projects as $project)
|
||||
<div class="relative">
|
||||
<a href="{{ route('project.todo.index', $project) }}" class="card-link">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 hover:shadow-md transition duration-300 ease-in-out transform hover:-translate-y-1">
|
||||
<div class="text-gray-800 dark:text-gray-100">
|
||||
<div class="mb-4">
|
||||
<h3 class="font-semibold text-lg mb-2">{{ $project->name }}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">{{ $project->description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<form action="{{ route('project.destroy', $project) }}" method="POST"
|
||||
class="delete-project-form absolute top-1 right-1">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="button"
|
||||
class="delete-button text-red-600 hover:text-red-800 transition duration-300 ease-in-out">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="modal hidden">
|
||||
<!-- Small Popover, with a background that is visible when modal is open -->
|
||||
<div class="popover popover-sm bg-white dark:bg-gray-800 shadow-lg rounded-lg p-6">
|
||||
<p class="mb-4">Are you sure you want to delete this project?</p>
|
||||
<button type="submit"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded mr-2">
|
||||
Delete
|
||||
</button>
|
||||
<button type="button"
|
||||
class="cancel-button bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const deleteButtons = document.querySelectorAll('.delete-button');
|
||||
const cancelButtons = document.querySelectorAll('.cancel-button');
|
||||
const modals = document.querySelectorAll('.modal');
|
||||
|
||||
deleteButtons.forEach((deleteButton, index) => {
|
||||
deleteButton.addEventListener('click', () => {
|
||||
modals[index].classList.remove('hidden');
|
||||
});
|
||||
|
||||
cancelButtons[index].addEventListener('click', () => {
|
||||
modals[index].classList.add('hidden');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 mt-4">
|
||||
<h2 class="text-2xl font-semibold mb-4 text-red-500 dark:text-red-400">
|
||||
Not Completed ({{ $todos->count() }})
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
@foreach ($todos as $todo)
|
||||
@if (!$todo->completed_at)
|
||||
<a href="{{ route('project.todo.edit', [$project->id, $todo->id]) }}" class="block">:
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<!-- Checkbox to toggle completed at -->
|
||||
<form action="{{ route('project.todo.update', [$project->id, $todo->id]) }}"
|
||||
method="POST"
|
||||
class="toggle-completed-form">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="completed_at"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
onclick="this.form.submit()" {{ $todo->completed_at ? 'checked' : ''
|
||||
}}>
|
||||
<span class="ml-2 text-sm text-gray-700"></span>
|
||||
</label>
|
||||
</form>
|
||||
<span
|
||||
class="ml-2 text-xl font-bold text-gray-800 dark:text-gray-100">{{ $todo->title }}
|
||||
<!-- Project name and link to project beside todo title as a badge with a blue background -->
|
||||
<span
|
||||
class="ml-2 text-sm font-semibold text-blue-600 bg-blue-100 rounded-full px-2 py-1">{{ $todo->project->name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
@php
|
||||
if ($todo->due_start && $todo->due_end) {
|
||||
$due = \Carbon\Carbon::parse($todo->due_end);
|
||||
$now = now();
|
||||
$timeRemaining = $due->isFuture() ? $now->diffForHumans($due, true) :
|
||||
$due->diffForHumans($now,
|
||||
true);
|
||||
} elseif ($todo->due_start) {
|
||||
$due = \Carbon\Carbon::parse($todo->due_start);
|
||||
$now = now();
|
||||
$timeRemaining = $due->isFuture() ? $now->diffForHumans($due, true) :
|
||||
$due->diffForHumans($now,
|
||||
true);
|
||||
} elseif ($todo->due_end) {
|
||||
$due = \Carbon\Carbon::parse($todo->due_end);
|
||||
$now = now();
|
||||
$timeRemaining = $due->isFuture() ? $now->diffForHumans($due, true) :
|
||||
$due->diffForHumans($now,
|
||||
true);
|
||||
} else {
|
||||
// If there is no due_start or due_end, set $timeRemaining to null
|
||||
$timeRemaining = null;
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if ($timeRemaining !== null)
|
||||
@if ($due->isFuture())
|
||||
<p class="text-sm text-green-600">{{ $timeRemaining }} remaining</p>
|
||||
@else
|
||||
<p class="text-sm text-red-600">{{ $timeRemaining }} ago</p>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 mt-4">
|
||||
<h2 class="text-2xl font-semibold mb-4 text-green-500 dark:text-green-400">
|
||||
Completed Today
|
||||
({{ $completed->count() }})
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
@foreach ($completed as $todo)
|
||||
<a href="{{ route('project.todo.edit', [$project->id, $todo->id]) }}" class="block">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<form action="{{ route('project.todo.update', [$project->id, $todo->id]) }}"
|
||||
method="POST"
|
||||
class="toggle-completed-form">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="completed_at" id="completed_at_{{ $todo->id }}"
|
||||
class="w-4 h-4 text-green-600 bg-gray-100 border-gray-300 rounded focus:ring-green-500 dark:focus:ring-green-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
onchange="this.form.submit()" {{ $todo->completed_at ? 'checked' : ''
|
||||
}}>
|
||||
<span class="ml-2 text-sm text-gray-700"></span>
|
||||
</label>
|
||||
</form>
|
||||
<span
|
||||
class="ml-2 text-2xl font-bold text-gray-800 dark:text-gray-100">
|
||||
{{ $todo->title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-app-layout>
|
|
@ -0,0 +1,155 @@
|
|||
<x-app-layout>
|
||||
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Todo List for Project:') }} <span class="font-bold text-blue-500 dark:text-blue-400"
|
||||
>{{ $project->name }}</span>
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<!-- Flash data for success and error messages -->
|
||||
@if(session()->has('success'))
|
||||
<div class="bg-green-500 text-white p-4 rounded-lg mb-6 text-center">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@elseif(session()->has('error'))
|
||||
<div class="bg-red-500 text-white p-4 rounded-lg mb-6 text-center">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 mb-3">
|
||||
<form method="POST" action="{{ route('project.todo.store', $project->id) }}">
|
||||
@csrf
|
||||
<div class="text-gray-800 dark:text-gray-100">
|
||||
<div class="mb-4 flex items-center">
|
||||
<input type="text" name="title" id="title" placeholder="Your Awesome Todo"
|
||||
class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-blue-500 text-gray-900 dark:text-gray-100 rounded-lg w-full p-4 @error('title') border-red-500 @enderror"
|
||||
value="{{ old('title') }}" required autofocus>
|
||||
|
||||
<!-- Plus button to submit the form or go to todo.create if title is empty -->
|
||||
<button type="submit"
|
||||
class="ml-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
onclick="event.preventDefault();
|
||||
document.getElementById('title').value.trim() === '' ? window.location.href = '{{ route('project.todo.create', $project->id) }}' : this.form.submit();">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-full inline-block">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@error('title')
|
||||
<div class="text-red-500 mt-2 text-sm">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 mb-3">
|
||||
<h2 class="text-2xl font-semibold mb-4 text-red-500 dark:text-red-400">
|
||||
Not Completed ({{ $todos->count() }})
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
@foreach ($todos as $todo)
|
||||
@if (!$todo->completed_at)
|
||||
<a href="{{ route('project.todo.edit', [$project->id, $todo->id]) }}" class="block">:
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<!-- Checkbox to toggle completed at -->
|
||||
<form action="{{ route('project.todo.update', [$project->id, $todo->id]) }}"
|
||||
method="POST"
|
||||
class="toggle-completed-form">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="completed_at"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
onclick="this.form.submit()" {{ $todo->completed_at ? 'checked' : '' }}>
|
||||
<span class="ml-2 text-sm text-gray-700"></span>
|
||||
</label>
|
||||
</form>
|
||||
<span class="ml-2 text-xl font-bold text-gray-800 dark:text-gray-100">{{ $todo->title }}</span>
|
||||
</div>
|
||||
<div>
|
||||
@php
|
||||
if ($todo->due_start && $todo->due_end) {
|
||||
$due = \Carbon\Carbon::parse($todo->due_end);
|
||||
$now = now();
|
||||
$timeRemaining = $due->isFuture() ? $now->diffForHumans($due, true) : $due->diffForHumans($now,
|
||||
true);
|
||||
} elseif ($todo->due_start) {
|
||||
$due = \Carbon\Carbon::parse($todo->due_start);
|
||||
$now = now();
|
||||
$timeRemaining = $due->isFuture() ? $now->diffForHumans($due, true) : $due->diffForHumans($now,
|
||||
true);
|
||||
} elseif ($todo->due_end) {
|
||||
$due = \Carbon\Carbon::parse($todo->due_end);
|
||||
$now = now();
|
||||
$timeRemaining = $due->isFuture() ? $now->diffForHumans($due, true) : $due->diffForHumans($now,
|
||||
true);
|
||||
} else {
|
||||
// If there is no due_start or due_end, set $timeRemaining to null
|
||||
$timeRemaining = null;
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if ($timeRemaining !== null)
|
||||
@if ($due->isFuture())
|
||||
<p class="text-sm text-green-600">{{ $timeRemaining }} remaining</p>
|
||||
@else
|
||||
<p class="text-sm text-red-600">{{ $timeRemaining }} ago</p>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
|
||||
<h2 class="text-2xl font-semibold mb-4 text-green-500 dark:text-green-400">
|
||||
Completed Today
|
||||
({{ $completed->count() }})
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
@foreach ($completed as $todo)
|
||||
<a href="{{ route('project.todo.edit', [$project->id, $todo->id]) }}" class="block">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<form action="{{ route('project.todo.update', [$project->id, $todo->id]) }}"
|
||||
method="POST"
|
||||
class="toggle-completed-form">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="completed_at" id="completed_at_{{ $todo->id }}"
|
||||
class="w-4 h-4 text-green-600 bg-gray-100 border-gray-300 rounded focus:ring-green-500 dark:focus:ring-green-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
onchange="this.form.submit()" {{ $todo->completed_at ? 'checked' : ''
|
||||
}}>
|
||||
<span class="ml-2 text-sm text-gray-700"></span>
|
||||
</label>
|
||||
</form>
|
||||
<span
|
||||
class="ml-2 text-2xl font-bold text-gray-800 dark:text-gray-100">
|
||||
{{ $todo->title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
|
@ -41,7 +41,7 @@
|
|||
<label for="due_start" class="block mb-2 font-semibold">Due Start</label>
|
||||
<input type="datetime-local" name="due_start" id="due_start" placeholder="Due Start"
|
||||
class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-blue-500 rounded-lg w-full p-4 @error('due_start') border-red-500 @enderror"
|
||||
value="{{ old('due_start', $todo->due_start ? date('Y-m-d\TH:i', strtotime($todo->due_start)) : '') }}">
|
||||
value="{{ old('due_start', $todo->due_start ? date('Y-m-d\TH:i', $todo->due_start) : '') }}">
|
||||
@error('due_start')
|
||||
<div class="text-red-500 mt-2 text-sm">
|
||||
{{ $message }}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\ProjectController;
|
||||
use App\Http\Controllers\ProjectTodoController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\TodoController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -16,7 +17,7 @@ use App\Http\Controllers\TodoController;
|
|||
*/
|
||||
|
||||
Route::get('/', function () {
|
||||
return redirect(route('todo.index'));
|
||||
return redirect(route('project.index'));
|
||||
});
|
||||
|
||||
Route::get('/dashboard', function () {
|
||||
|
@ -24,19 +25,19 @@ Route::get('/dashboard', function () {
|
|||
})->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');
|
||||
|
|
Loading…
Reference in New Issue