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

View File

@ -30,7 +30,7 @@ class Pomos extends Component
$user = User::find(auth()->id());
$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->where('user_id', $user->id);
});

View File

@ -30,17 +30,20 @@ class Project extends Model
*/
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,
projectUser::class,
'project_user',
'project_id',
'id',
'id',
'user_id'
);
}

View File

@ -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\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
@ -50,9 +51,13 @@ class Todo extends Model
->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
{
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
{
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
{
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
{
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
{
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\Todo;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response;
class TodoPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*/
@ -22,7 +25,8 @@ class TodoPolicy
*/
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.
*/
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)
return false;
$project = $todo->projects->first();
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
{
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
{
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
{
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)
<div class="border rounded-lg p-4 shadow-md hover:bg-blue-100 dark:hover:bg-gray-700">
<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 }}
</a>
</h3>
</div>
<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') }}

View File

@ -5,56 +5,41 @@
</h2>
<div class="space-y-4" id="todo-container">
@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">
<div
class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 flex items-center justify-between">
if ($todo->due_start && $todo->due_end) {
$due = \Carbon\Carbon::parse($todo->due_end);
} 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">
<form action="{{ route('project.todo.update', [$todo->project->id, $todo->id]) }}"
method="POST"
<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' : ''
}}>
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>
<span class="ml-2 text-2xl 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>
@ -65,9 +50,11 @@
</div>
</div>
</a>
@endif
@endforeach
</div>
@if($todos->hasMorePages())
<div class="invisible">
<button wire:click.prevent="loadMore"

View File

@ -6,18 +6,37 @@
Not Completed ({{ $todos->count() }})
</h2>
<div class="space-y-4 mb-4">
<div class="space-y-4" id="todo-container">
@foreach ($todos as $todo)
@if ($todo->projects->isNotEmpty())
@php
$project = $todo->projects->first();
@endphp
@endif
@if (!$todo->completed_at)
<a href="{{ route('project.todo.edit', [$todo->project->id, $todo->id]) }}" class="block">
@php
$due = null;
if ($todo->due_start && $todo->due_end) {
$due = \Carbon\Carbon::parse($todo->due_end);
} 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="p-6 bg-white dark:bg-gray-800 shadow-sm rounded-lg flex items-center">
<!-- Checkbox to toggle completed at -->
<form action="{{ route('project.todo.update', [$todo->project->id, $todo->id]) }}"
method="POST"
class="toggle-completed-form">
<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">
<!-- Larger Checkbox -->
<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' : '' }}>
@ -26,52 +45,24 @@
</form>
<span
class="ml-2 text-xl font-bold text-gray-800 dark:text-gray-100">{{ $todo->title }}</span>
<!-- Badge smaller width, below the title -->
<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">
{{ $todo->project->name }}
{{ $project->name }}
</div>
</div>
<!-- Date -->
<div class="relative px-6 pb-6">
@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
<p class="text-sm {{ $due->isFuture() ? 'text-green-600' : 'text-red-600' }}">{{
$timeRemaining }} {{ $due->isFuture() ? 'remaining' : 'ago' }}</p>
@endif
</div>
</a>
@endif
@endforeach
</div>
</div>
</div>
<!-- Completed Todos Section -->
@ -81,9 +72,15 @@
</h2>
<div class="space-y-4">
@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">
<form action="{{ route('project.todo.update', [$todo->project->id, $todo->id]) }}"
<form action="{{ route('project.todo.update', [$project->id, $todo->id]) }}"
method="POST"
class="toggle-completed-form">
@csrf
@ -102,7 +99,7 @@
<!-- Badge -->
<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">
{{ $todo->project->name }}
{{ $project->name }}
</div>
</div>

View File

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