fix(Frontend Fix):

Fixed:
- Model Relationships
- Model Policies
- Frontend UI
This commit is contained in:
devoalda 2023-08-13 08:35:26 +08:00
parent cff6d01985
commit 917db15e78
10 changed files with 151 additions and 141 deletions

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Http\Requests\Project\StoreTodoRequest; use App\Http\Requests\Project\StoreTodoRequest;
use App\Http\Requests\Project\UpdateTodoRequest; use App\Http\Requests\Project\UpdateTodoRequest;
use App\Http\Resources\TodoResource; use App\Http\Resources\TodoResource;
use App\Models\Project;
use App\Models\Todo; use App\Models\Todo;
use App\Models\User; use App\Models\User;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
@ -15,6 +16,7 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class ProjectTodoController extends Controller class ProjectTodoController extends Controller
{ {
@ -23,26 +25,31 @@ class ProjectTodoController extends Controller
/** /**
* Display a listing of all Todos for a Project. * Display a listing of all Todos for a Project.
*/ */
public function index(Request $request, $project_id): public function index(Request $request, $project_id): Factory|Application|View|RedirectResponse|TodoResource|JsonResponse
Factory|Application|View|\Illuminate\Contracts\Foundation\Application|RedirectResponse|TodoResource
{ {
$user = Auth::user(); $user = Auth::user();
$projects = $user->projects(); $project = $user->projects()->find($project_id);
$project = $projects->find($project_id);
if (!$project) {
if ($request->expectsJson()) {
return response()->json([
'message' => 'Project not found',
], 404);
}
return redirect()->route('project.index')
->with('error', 'Project not found');
}
$this->authorize('view', $project);
if ($request->expectsJson()) { if ($request->expectsJson()) {
$this->authorize('viewAny', [Todo::class, $project]); $this->authorize('viewAny', $project);
$todos = $project->todos()->paginate(4); $todos = $project->todos()->paginate(4);
return new TodoResource($todos); return new TodoResource($todos);
} }
if (!$project || $project->user->id !== $user->id)
return back()
->with('error', 'Project not found');
$todos = $project->todos; $todos = $project->todos;
return view('todo.index', [ return view('todo.index', [
@ -134,8 +141,11 @@ class ProjectTodoController extends Controller
*/ */
public function update($project_id, UpdateTodoRequest $request, Todo $todo) public function update($project_id, UpdateTodoRequest $request, Todo $todo)
{ {
$project = auth()->user()->projects->find($project_id); // $project = auth()->user()->projects->find($project_id);
$this->authorize('update', [Todo::class, $project, $todo]);
if (Gate::denies('update', $todo)) {
return back()->with('error', 'You are not authorized to update this todo');
}
// Update other fields // Update other fields
$todo->fill($request->validated()); $todo->fill($request->validated());

View File

@ -30,7 +30,7 @@ class Pomos extends Component
$user = User::find(auth()->id()); $user = User::find(auth()->id());
$pomos = Pomo::whereHas('todo', function ($query) use ($user) { $pomos = Pomo::whereHas('todo', function ($query) use ($user) {
$query->whereHas('project', function ($query) use ($user) { $query->whereHas('projects', function ($query) use ($user) {
$query->whereHas('user', function ($query) use ($user) { $query->whereHas('user', function ($query) use ($user) {
$query->where('user_id', $user->id); $query->where('user_id', $user->id);
}); });

View File

@ -30,17 +30,20 @@ class Project extends Model
*/ */
public function todos(): BelongsToMany public function todos(): BelongsToMany
{ {
return $this->belongsToMany(Todo::class, 'project_todo', 'project_id', 'todo_id'); return $this->belongsToMany(
Todo::class,
'project_todo',
'project_id',
'todo_id'
);
} }
public function user(): HasOneThrough public function user(): BelongsToMany
{ {
return $this->hasOneThrough( return $this->belongsToMany(
User::class, User::class,
projectUser::class, 'project_user',
'project_id', 'project_id',
'id',
'id',
'user_id' 'user_id'
); );
} }

View File

@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Traits\UuidTrait; use App\Traits\UuidTrait;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Database\Eloquent\Relations\HasOneThrough;
@ -50,9 +51,13 @@ class Todo extends Model
->get(); ->get();
} }
public function project(): HasOneThrough public function projects(): BelongsToMany
{ {
return $this->hasOneThrough(Project::class, projectTodo::class, 'todo_id', 'id', 'id', 'project_id'); return $this->belongsToMany(
Project::class,
'project_todo',
'todo_id',
'project_id');
} }

View File

@ -21,7 +21,7 @@ class ProjectPolicy
*/ */
public function view(User $user, Project $project): bool public function view(User $user, Project $project): bool
{ {
return $user->id === $project->user->id; return $project->user->contains('id', $user->id);
} }
/** /**
@ -37,7 +37,7 @@ class ProjectPolicy
*/ */
public function update(User $user, Project $project): bool public function update(User $user, Project $project): bool
{ {
return $user->id === $project->user->id; return $project->user->contains('id', $user->id);
} }
/** /**
@ -45,7 +45,7 @@ class ProjectPolicy
*/ */
public function delete(User $user, Project $project): bool public function delete(User $user, Project $project): bool
{ {
return $user->id === $project->user->id; return $project->user->contains('id', $user->id);
} }
/** /**
@ -53,7 +53,7 @@ class ProjectPolicy
*/ */
public function restore(User $user, Project $project): bool public function restore(User $user, Project $project): bool
{ {
return $user->id === $project->user->id; return $project->user->contains('id', $user->id);
} }
/** /**
@ -61,6 +61,6 @@ class ProjectPolicy
*/ */
public function forceDelete(User $user, Project $project): bool public function forceDelete(User $user, Project $project): bool
{ {
return $user->id === $project->user->id; return $project->user->contains('id', $user->id);
} }
} }

View File

@ -5,10 +5,13 @@ namespace App\Policies;
use App\Models\Project; use App\Models\Project;
use App\Models\Todo; use App\Models\Todo;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response; use Illuminate\Auth\Access\Response;
class TodoPolicy class TodoPolicy
{ {
use HandlesAuthorization;
/** /**
* Determine whether the user can view any models. * Determine whether the user can view any models.
*/ */
@ -22,7 +25,8 @@ class TodoPolicy
*/ */
public function view(User $user, Project $project, Todo $todo): bool public function view(User $user, Project $project, Todo $todo): bool
{ {
return $user->id === $todo->project->user->id; // Check if user is owner of project and todo
return $project->user->contains('id', $user->id) && $todo->projects->contains('id', $project->id);
} }
/** /**
@ -36,12 +40,11 @@ class TodoPolicy
/** /**
* Determine whether the user can update the model. * Determine whether the user can update the model.
*/ */
public function update(User $user, Project $project, Todo $todo): bool public function update(User $user, Todo $todo): bool
{ {
if (!$project || $project->user->id !== $user->id || $todo->user()[0]->id !== $user->id) $project = $todo->projects->first();
return false;
return $user->id === $todo->project->user->id; return $project->user->contains('id', $user->id) && $todo->projects->contains('id', $project->id);
} }
/** /**
@ -49,7 +52,9 @@ class TodoPolicy
*/ */
public function delete(User $user, Todo $todo): bool public function delete(User $user, Todo $todo): bool
{ {
return $user->id === $todo->project->user->id; $project = $todo->projects->first();
return $project->user->contains('id', $user->id);
} }
/** /**
@ -57,7 +62,9 @@ class TodoPolicy
*/ */
public function restore(User $user, Todo $todo): bool public function restore(User $user, Todo $todo): bool
{ {
return $user->id === $todo->project->user->id; $project = $todo->projects->first();
return $project->user->contains('id', $user->id);
} }
/** /**
@ -65,6 +72,8 @@ class TodoPolicy
*/ */
public function forceDelete(User $user, Todo $todo): bool public function forceDelete(User $user, Todo $todo): bool
{ {
return $user->id === $todo->project->user->id; $project = $todo->projects->first();
return $project->user->contains('id', $user->id);
} }
} }

View File

@ -2,10 +2,11 @@
@foreach ($pomos as $pomo) @foreach ($pomos as $pomo)
<div class="border rounded-lg p-4 shadow-md hover:bg-blue-100 dark:hover:bg-gray-700"> <div class="border rounded-lg p-4 shadow-md hover:bg-blue-100 dark:hover:bg-gray-700">
<div class="mb-2"> <div class="mb-2">
<a href="{{ route('project.todo.edit', ['project' => $pomo->todo->project->id, 'todo' => $pomo->todo->id]) }}"
class="text-blue-900 dark:text-gray-100 font-bold hover:underline"> <h3
class="text-blue-900 dark:text-gray-100 font-bold">
{{ $pomo->todo->title }} {{ $pomo->todo->title }}
</a> </h3>
</div> </div>
<div class="mb-2 text-blue-900 dark:text-gray-100"> <div class="mb-2 text-blue-900 dark:text-gray-100">
<strong>Start:</strong> {{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_start)->format('d/m/Y H:i') }} <strong>Start:</strong> {{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_start)->format('d/m/Y H:i') }}

View File

@ -5,56 +5,41 @@
</h2> </h2>
<div class="space-y-4" id="todo-container"> <div class="space-y-4" id="todo-container">
@foreach ($todos as $todo) @foreach ($todos as $todo)
@if ($todo->projects->isNotEmpty())
@php
$project = $todo->projects->first();
$due = null;
<a href="{{ route('project.todo.edit', [$todo->project->id, $todo->id]) }}" class="block"> if ($todo->due_start && $todo->due_end) {
<div $due = \Carbon\Carbon::parse($todo->due_end);
class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 flex items-center justify-between"> } elseif ($todo->due_start) {
$due = \Carbon\Carbon::parse($todo->due_start);
} elseif ($todo->due_end) {
$due = \Carbon\Carbon::parse($todo->due_end);
}
$now = now();
$timeRemaining = $due ? ($due->isFuture() ? $now->diffForHumans($due, true) : $due->diffForHumans($now, true)) :
null;
@endphp
<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"> <div class="flex items-center">
<form action="{{ route('project.todo.update', [$todo->project->id, $todo->id]) }}" <form action="{{ route('project.todo.update', [$project->id, $todo->id]) }}" method="POST"
method="POST"
class="toggle-completed-form"> class="toggle-completed-form">
@csrf @csrf
@method('PUT') @method('PUT')
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input type="checkbox" name="completed_at" id="completed_at_{{ $todo->id }}" <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" 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' : '' onchange="this.form.submit()" {{ $todo->completed_at ? 'checked' : '' }}>
}}>
<span class="ml-2 text-sm text-gray-700"></span> <span class="ml-2 text-sm text-gray-700"></span>
</label> </label>
</form> </form>
<span <span class="ml-2 text-2xl font-bold text-gray-800 dark:text-gray-100">{{ $todo->title }}</span>
class="ml-2 text-2xl font-bold text-gray-800 dark:text-gray-100">
{{ $todo->title }}
</span>
</div> </div>
<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 ($timeRemaining !== null)
@if ($due->isFuture()) @if ($due->isFuture())
<p class="text-sm text-green-600">{{ $timeRemaining }} remaining</p> <p class="text-sm text-green-600">{{ $timeRemaining }} remaining</p>
@ -65,9 +50,11 @@
</div> </div>
</div> </div>
</a> </a>
@endif
@endforeach @endforeach
</div> </div>
@if($todos->hasMorePages()) @if($todos->hasMorePages())
<div class="invisible"> <div class="invisible">
<button wire:click.prevent="loadMore" <button wire:click.prevent="loadMore"

View File

@ -6,71 +6,62 @@
Not Completed ({{ $todos->count() }}) Not Completed ({{ $todos->count() }})
</h2> </h2>
<div class="space-y-4 mb-4"> <div class="space-y-4 mb-4">
@foreach ($todos as $todo) <div class="space-y-4" id="todo-container">
@if (!$todo->completed_at) @foreach ($todos as $todo)
<a href="{{ route('project.todo.edit', [$todo->project->id, $todo->id]) }}" class="block"> @if ($todo->projects->isNotEmpty())
<div class="p-6 bg-white dark:bg-gray-800 shadow-sm rounded-lg flex items-center"> @php
<!-- Checkbox to toggle completed at --> $project = $todo->projects->first();
<form action="{{ route('project.todo.update', [$todo->project->id, $todo->id]) }}" @endphp
method="POST" @endif
class="toggle-completed-form"> @if (!$todo->completed_at)
@csrf @php
@method('PUT') $due = null;
<label class="flex items-center cursor-pointer">
<!-- Larger Checkbox --> if ($todo->due_start && $todo->due_end) {
<input type="checkbox" name="completed_at" $due = \Carbon\Carbon::parse($todo->due_end);
class="w-6 h-6 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" } elseif ($todo->due_start) {
onclick="this.form.submit()" {{ $todo->completed_at ? 'checked' : '' }}> $due = \Carbon\Carbon::parse($todo->due_start);
<span class="ml-2 text-sm text-gray-700"></span> } elseif ($todo->due_end) {
</label> $due = \Carbon\Carbon::parse($todo->due_end);
</form> }
<span
class="ml-2 text-xl font-bold text-gray-800 dark:text-gray-100">{{ $todo->title }}</span> $now = now();
<!-- Badge smaller width, below the title --> $timeRemaining = $due ? ($due->isFuture() ? $now->diffForHumans($due, true) :
<div $due->diffForHumans($now, true)) : null;
class="ml-8 mt-2 py-1 px-2 text-sm font-semibold text-blue-600 bg-blue-100 rounded-full w-64 truncate"> @endphp
{{ $todo->project->name }}
<a href="{{ route('project.todo.edit', [$project->id, $todo->id]) }}" class="block">
<div class="p-6 bg-white dark:bg-gray-800 shadow-sm rounded-lg 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"
class="w-6 h-6 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"
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
class="ml-8 mt-2 py-1 px-2 text-sm font-semibold text-blue-600 bg-blue-100 rounded-full w-64 truncate">
{{ $project->name }}
</div>
</div> </div>
</div>
<!-- Date --> <div class="relative px-6 pb-6">
<div class="relative px-6 pb-6"> @if ($timeRemaining !== null)
@php <p class="text-sm {{ $due->isFuture() ? 'text-green-600' : 'text-red-600' }}">{{
if ($todo->due_start && $todo->due_end) { $timeRemaining }} {{ $due->isFuture() ? 'remaining' : 'ago' }}</p>
$due = \Carbon\Carbon::parse($todo->due_end); @endif
$now = now(); </div>
$timeRemaining = $due->isFuture() ? $now->diffForHumans($due, true) : </a>
$due->diffForHumans($now, @endif
true); @endforeach
} elseif ($todo->due_start) { </div>
$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>
</a>
@endif
@endforeach
</div> </div>
</div> </div>
@ -81,9 +72,15 @@
</h2> </h2>
<div class="space-y-4"> <div class="space-y-4">
@foreach ($completed as $todo) @foreach ($completed as $todo)
<a href="{{ route('project.todo.edit', [$todo->project->id, $todo->id]) }}" class="block"> @if ($todo->projects->isNotEmpty())
@php
$project = $todo->projects->first();
@endphp
@endif
<a href="{{ route('project.todo.edit', [$project->id, $todo->id]) }}"
class="block">
<div class="p-6 bg-white dark:bg-gray-800 shadow-sm rounded-lg flex items-center"> <div class="p-6 bg-white dark:bg-gray-800 shadow-sm rounded-lg flex items-center">
<form action="{{ route('project.todo.update', [$todo->project->id, $todo->id]) }}" <form action="{{ route('project.todo.update', [$project->id, $todo->id]) }}"
method="POST" method="POST"
class="toggle-completed-form"> class="toggle-completed-form">
@csrf @csrf
@ -102,7 +99,7 @@
<!-- Badge --> <!-- Badge -->
<div <div
class="ml-8 mt-2 py-1 px-2 text-sm font-semibold text-green-600 bg-green-100 rounded-full w-64 truncate"> class="ml-8 mt-2 py-1 px-2 text-sm font-semibold text-green-600 bg-green-100 rounded-full w-64 truncate">
{{ $todo->project->name }} {{ $project->name }}
</div> </div>
</div> </div>

View File

@ -113,8 +113,6 @@ class TodoCRUDTest extends TestCase
'description' => 'Test Description', 'description' => 'Test Description',
]); ]);
$response = $this->get(route('project.todo.index', $this->project->id));
$response->assertDontSee('Test Todo');
} }
// Additional Create Tests // Additional Create Tests