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\Models\Project;
use App\Models\Todo;
use Carbon\Carbon;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
@ -30,10 +32,10 @@ class ProjectController extends Controller
$todos = $user->todos()
->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();
return Response::json([
'view' => $view,
@ -45,7 +47,7 @@ class ProjectController extends Controller
'projects' => $projects,
'todos' => $todos->whereNull('completed_at')->values(),
'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(),
]);
}
@ -63,31 +65,32 @@ class ProjectController extends Controller
*/
public function store(StoreProjectRequest $request): RedirectResponse
{
$user = User::find(auth()->user()->id);
$data = $request->validated();
$user->projects()->create($data);
auth()->user()->projects()->create($data);
return redirect()->route('project.index')
->with('info', 'Project created!');
}
/**
* TODO: Complete this method (if needed)
* Display the specified resource.
* @throws AuthorizationException
*/
public function show(Project $project): RedirectResponse
{
$this->authorize('view', $project);
return redirect()->route('project.index');
}
/**
* TODO: Complete this method (if needed)
* 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', [
'project' => $project,
]);
@ -98,14 +101,17 @@ class ProjectController extends Controller
*/
public function update(UpdateProjectRequest $request, Project $project): RedirectResponse
{
$user = User::find(auth()->user()->id);
$projects = $user->projects;
$project = $projects->find($project->id);
$this->authorize('update', $project);
$data = $request->validated();
$data = $request->validatedWithCompletedAt();
$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!');
}
@ -114,9 +120,7 @@ class ProjectController extends Controller
*/
public function destroy(Project $project)
{
$user = User::find(auth()->user()->id);
$projects = $user->projects;
$project = $projects->find($project->id);
$this->authorize('delete', $project);
$project->delete();

View File

@ -10,7 +10,6 @@ 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
@ -77,8 +76,7 @@ class ProjectTodoController extends Controller
$projects = $user->projects;
$project = $projects->find($project_id);
if (!$project || $project->user->id !== auth()->user()->id || $todo->user()[0]->id !== auth()->user()->id)
return back()->with('error', 'Project/Todo not found');
$this->authorize('view', [Todo::class, $project, $todo]);
return view('todo.show', compact('project', 'todo'));
}
@ -88,12 +86,9 @@ class ProjectTodoController extends Controller
*/
public function edit($project_id, Todo $todo)
{
$user = User::find(auth()->user()->id);
$projects = $user->projects;
$projects = auth()->user()->projects;
$project = $projects->find($project_id);
$this->authorize('update', [Todo::class, $project, $todo]);
$this->authorize('view', [Todo::class, $project, $todo]);
return view('todo.edit', compact('project', 'todo'));
}
@ -110,6 +105,15 @@ class ProjectTodoController extends Controller
// Update other fields
$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();

View File

@ -22,17 +22,23 @@ class AveTodoPerProject extends Component
$projects = $user->projects;
$project_count = $projects->count();
if ($project_count === 0) {
$this->ave_todo_count = 0;
return;
}
// Average number of todos per project
$ave_todo_count = function ($projects) {
$todo_count = 0;
foreach ($projects as $project) {
$todo_count += $project->todos->count();
}
return $todo_count / $projects->count();
};
$this->ave_todo_count = $ave_todo_count($projects);
}
public function render()
{
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) {
return $todo->pomos->count();
});
$this->ave_pomo_count = $ave_pomo_count;
$this->ave_pomo_count = $ave_pomo_count ?? 0;
}
public function render()

View File

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

View File

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

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests\Project;
use Carbon\Carbon;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
@ -25,6 +26,29 @@ class UpdateProjectRequest extends FormRequest
return [
'name' => 'required|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([
'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 [
'title' => 'nullable|string|max:255',
'description' => 'nullable|string|max:255',
'due_start' => 'nullable',
'due_end' => 'nullable',
'due_start' => 'nullable|date',
'due_end' => 'nullable|date|after_or_equal:due_start',
'completed_at' => 'nullable',
];
}

View File

@ -21,6 +21,7 @@ class Project extends Model
protected $fillable = [
'name',
'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.
*/
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;
}

View File

@ -19,6 +19,7 @@ class ProjectFactory extends Factory
return [
'name' => $this->faker->sentence(3),
'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
</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 class="flex justify-end mt-4 space-x-4">