feature(Project Completion):

Added Ability to mark completion for project
This commit is contained in:
devoalda 2023-08-11 10:28:57 +08:00
parent bc72c6c12e
commit da2715f240
14 changed files with 183 additions and 33 deletions

View File

@ -6,6 +6,8 @@ use App\Http\Requests\Project\StoreProjectRequest;
use App\Http\Requests\Project\UpdateProjectRequest; use App\Http\Requests\Project\UpdateProjectRequest;
use App\Models\Project; use App\Models\Project;
use App\Models\Todo; use App\Models\Todo;
use Carbon\Carbon;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
@ -30,10 +32,10 @@ class ProjectController extends Controller
$todos = $user->todos() $todos = $user->todos()
->map(function ($todo) { ->map(function ($todo) {
return Todo::find($todo->id); return Todo::find($todo->id);
}); });
if ($request->ajax()){ if ($request->ajax()) {
$view = view('project.load-projects', compact('projects'))->render(); $view = view('project.load-projects', compact('projects'))->render();
return Response::json([ return Response::json([
'view' => $view, 'view' => $view,
@ -45,7 +47,7 @@ class ProjectController extends Controller
'projects' => $projects, 'projects' => $projects,
'todos' => $todos->whereNull('completed_at')->values(), 'todos' => $todos->whereNull('completed_at')->values(),
'completed' => $todos->whereNotNull('completed_at') 'completed' => $todos->whereNotNull('completed_at')
->whereBetween('completed_at', [strtotime('today midnight'), strtotime('today midnight + 1 day')]) ->whereBetween('completed_at', [strtotime('today midnight'), strtotime('today midnight + 1 day')])
->values(), ->values(),
]); ]);
} }
@ -63,31 +65,32 @@ class ProjectController extends Controller
*/ */
public function store(StoreProjectRequest $request): RedirectResponse public function store(StoreProjectRequest $request): RedirectResponse
{ {
$user = User::find(auth()->user()->id);
$data = $request->validated(); $data = $request->validated();
$user->projects()->create($data); auth()->user()->projects()->create($data);
return redirect()->route('project.index') return redirect()->route('project.index')
->with('info', 'Project created!'); ->with('info', 'Project created!');
} }
/** /**
* TODO: Complete this method (if needed)
* Display the specified resource. * Display the specified resource.
* @throws AuthorizationException
*/ */
public function show(Project $project): RedirectResponse public function show(Project $project): RedirectResponse
{ {
$this->authorize('view', $project);
return redirect()->route('project.index'); return redirect()->route('project.index');
} }
/** /**
* TODO: Complete this method (if needed)
* Show the form for editing the specified Project. * Show the form for editing the specified Project.
* @throws AuthorizationException
*/ */
public function edit(Project $project) public function edit(Project $project): View|\Illuminate\Foundation\Application|Factory|Application
{ {
$this->authorize('view', $project);
return view('project.edit', [ return view('project.edit', [
'project' => $project, 'project' => $project,
]); ]);
@ -98,14 +101,17 @@ class ProjectController extends Controller
*/ */
public function update(UpdateProjectRequest $request, Project $project): RedirectResponse public function update(UpdateProjectRequest $request, Project $project): RedirectResponse
{ {
$user = User::find(auth()->user()->id); $this->authorize('update', $project);
$projects = $user->projects;
$project = $projects->find($project->id);
$data = $request->validated(); $data = $request->validatedWithCompletedAt();
$project->update($data); $project->update($data);
// Complete all todos in project
if ($request->has('completed_at') && $request->completed_at) {
$project->todos()->update(['completed_at' => Carbon::now()->timestamp]);
}
return back()->with('info', 'Project updated!'); return back()->with('info', 'Project updated!');
} }
@ -114,9 +120,7 @@ class ProjectController extends Controller
*/ */
public function destroy(Project $project) public function destroy(Project $project)
{ {
$user = User::find(auth()->user()->id); $this->authorize('delete', $project);
$projects = $user->projects;
$project = $projects->find($project->id);
$project->delete(); $project->delete();

View File

@ -10,7 +10,6 @@ use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
class ProjectTodoController extends Controller class ProjectTodoController extends Controller
@ -77,8 +76,7 @@ class ProjectTodoController extends Controller
$projects = $user->projects; $projects = $user->projects;
$project = $projects->find($project_id); $project = $projects->find($project_id);
if (!$project || $project->user->id !== auth()->user()->id || $todo->user()[0]->id !== auth()->user()->id) $this->authorize('view', [Todo::class, $project, $todo]);
return back()->with('error', 'Project/Todo not found');
return view('todo.show', compact('project', 'todo')); return view('todo.show', compact('project', 'todo'));
} }
@ -88,12 +86,9 @@ class ProjectTodoController extends Controller
*/ */
public function edit($project_id, Todo $todo) public function edit($project_id, Todo $todo)
{ {
$user = User::find(auth()->user()->id); $projects = auth()->user()->projects;
$projects = $user->projects;
$project = $projects->find($project_id); $project = $projects->find($project_id);
$this->authorize('view', [Todo::class, $project, $todo]);
$this->authorize('update', [Todo::class, $project, $todo]);
return view('todo.edit', compact('project', 'todo')); return view('todo.edit', compact('project', 'todo'));
} }
@ -110,6 +105,15 @@ class ProjectTodoController extends Controller
// Update other fields // Update other fields
$todo->fill($request->validated()); $todo->fill($request->validated());
$todo->due_start = $request->due_start ?
strtotime(Carbon::parse($request->due_start)) :
($todo->due_start ?: null);
$todo->due_end = $request->due_end ?
strtotime(Carbon::parse($request->due_end)) :
($todo->due_end ?:
($todo->due_start ? strtotime(Carbon::parse($todo->due_start)) : null));
$todo->save(); $todo->save();

View File

@ -22,17 +22,23 @@ class AveTodoPerProject extends Component
$projects = $user->projects; $projects = $user->projects;
$project_count = $projects->count(); $project_count = $projects->count();
if ($project_count === 0) {
$this->ave_todo_count = 0;
return;
}
// Average number of todos per project // Average number of todos per project
$ave_todo_count = function ($projects) { $ave_todo_count = function ($projects) {
$todo_count = 0; $todo_count = 0;
foreach ($projects as $project) { foreach ($projects as $project) {
$todo_count += $project->todos->count(); $todo_count += $project->todos->count();
} }
return $todo_count / $projects->count(); return $todo_count / $projects->count();
}; };
$this->ave_todo_count = $ave_todo_count($projects); $this->ave_todo_count = $ave_todo_count($projects);
} }
public function render() public function render()
{ {
return view('livewire.dashboard.ave-todo-per-project'); return view('livewire.dashboard.ave-todo-per-project');

View File

@ -22,7 +22,7 @@ class PomoCount extends Component
$ave_pomo_count = $todos->avg(function ($todo) { $ave_pomo_count = $todos->avg(function ($todo) {
return $todo->pomos->count(); return $todo->pomos->count();
}); });
$this->ave_pomo_count = $ave_pomo_count; $this->ave_pomo_count = $ave_pomo_count ?? 0;
} }
public function render() public function render()

View File

@ -28,6 +28,11 @@ class PomoTime extends Component
$total_pomos = $pomos->count(); $total_pomos = $pomos->count();
if ($total_pomos === 0) {
$this->ave_pomo_time = 0;
return;
}
$total_time = 0; $total_time = 0;
foreach ($pomos as $pomo) { foreach ($pomos as $pomo) {

View File

@ -4,6 +4,7 @@ namespace App\Http\Requests\Project;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Carbon;
class StoreTodoRequest extends FormRequest class StoreTodoRequest extends FormRequest
{ {

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests\Project; namespace App\Http\Requests\Project;
use Carbon\Carbon;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
@ -25,6 +26,29 @@ class UpdateProjectRequest extends FormRequest
return [ return [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'nullable|string|max:255', 'description' => 'nullable|string|max:255',
'completed_at' => 'nullable'
]; ];
} }
public function validatedWithCompletedAt(): array
{
// Return safe data merged with completed_at to unix timestamp
return array_merge(
$this->validated(),
[
// Now or null
'completed_at' => $this->completed_at ? Carbon::now()->timestamp : null,
]
);
}
protected function passedValidation(): void
{
// Replace or add completed_at to the request, value is time now in unix format
if ($this->has('completed_at')) {
$this->request->add(['completed_at' => Carbon::now()->timestamp]);
} else {
$this->request->add(['completed_at' => null]);
}
}
} }

View File

@ -23,10 +23,6 @@ class UpdateTodoRequest extends FormRequest
{ {
$this->merge([ $this->merge([
'completed_at' => $this->completed_at ? strtotime(Carbon::parse('now')) : null, 'completed_at' => $this->completed_at ? strtotime(Carbon::parse('now')) : null,
'due_start' => $this->due_start ? strtotime(Carbon::parse($this->due_start)) : null,
'due_end' => $this->due_end ? strtotime(Carbon::parse($this->due_end)) :
($this->due_start ? strtotime(Carbon::parse($this->due_start)) : null),
]); ]);
} }
@ -40,8 +36,8 @@ class UpdateTodoRequest extends FormRequest
return [ return [
'title' => 'nullable|string|max:255', 'title' => 'nullable|string|max:255',
'description' => 'nullable|string|max:255', 'description' => 'nullable|string|max:255',
'due_start' => 'nullable', 'due_start' => 'nullable|date',
'due_end' => 'nullable', 'due_end' => 'nullable|date|after_or_equal:due_start',
'completed_at' => 'nullable', 'completed_at' => 'nullable',
]; ];
} }

View File

@ -21,6 +21,7 @@ class Project extends Model
protected $fillable = [ protected $fillable = [
'name', 'name',
'description', 'description',
'completed_at',
]; ];
/** /**

View File

@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Project;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ProjectPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
//
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Project $project): bool
{
return $user->id === $project->user->id;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Project $project): bool
{
return $user->id === $project->user->id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Project $project): bool
{
return $user->id === $project->user->id;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Project $project): bool
{
return $user->id === $project->user->id;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Project $project): bool
{
return $user->id === $project->user->id;
}
}

View File

@ -20,7 +20,7 @@ class TodoPolicy
/** /**
* Determine whether the user can view the model. * Determine whether the user can view the model.
*/ */
public function view(User $user, Todo $todo): bool public function view(User $user, Project $project, Todo $todo): bool
{ {
return $user->id === $todo->project->user->id; return $user->id === $todo->project->user->id;
} }

View File

@ -19,6 +19,7 @@ class ProjectFactory extends Factory
return [ return [
'name' => $this->faker->sentence(3), 'name' => $this->faker->sentence(3),
'description' => $this->faker->sentence(10), 'description' => $this->faker->sentence(10),
'completed_at' => $this->faker->boolean(20) ? $this->faker->unixTime() : null,
]; ];
} }
} }

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Add Completed at column
Schema::table('projects', function (Blueprint $table) {
$table->integer('completed_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Drop Completed at column
Schema::table('projects', function (Blueprint $table) {
$table->dropColumn('completed_at');
});
}
};

View File

@ -36,6 +36,18 @@
@enderror @enderror
</div> </div>
<!-- Completed Checkbox -->
<div class="mb-4">
<label for="completed_at" class="block mb-2 font-semibold">Completed</label>
<input type="checkbox" name="completed_at" id="completed_at"
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-6 h-6 p-4 @error('completed') border-red-500 @enderror"
{{ old('completed', $project->completed_at) ? 'checked' : '' }}>
@error('completed')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div> </div>
<div class="flex justify-end mt-4 space-x-4"> <div class="flex justify-end mt-4 space-x-4">