mirror of https://github.com/Devoalda/LaDo.git
feature(Pomo Feature):
Added Pomo Tables + Features + Test Cases Added Livewire support
This commit is contained in:
parent
0603b56602
commit
9df9c8d2f2
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StorePomoRequest;
|
||||
use App\Http\Requests\UpdatePomoRequest;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use App\Models\{
|
||||
Pomo,
|
||||
Project,
|
||||
User,
|
||||
Todo
|
||||
};
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PomoController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('pomo.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create($todo_id = null)
|
||||
{
|
||||
$this->authorize('create', Pomo::class);
|
||||
|
||||
return view('pomo.create', [
|
||||
'todo_id' => $todo_id,
|
||||
'pomo' => new Pomo(),
|
||||
'editing' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(StorePomoRequest $request)
|
||||
{
|
||||
// $this->authorize('create', Pomo::class);
|
||||
|
||||
// Convert due_start and end to unix timestamp and save
|
||||
$pomo = new Pomo();
|
||||
$pomo->todo_id = $request->todo_id;
|
||||
$pomo->pomo_start = strtotime($request->pomo_start);
|
||||
$pomo->pomo_end = strtotime($request->pomo_end);
|
||||
$pomo->notes = $request->notes;
|
||||
$pomo->save();
|
||||
|
||||
return redirect()->route('pomo.index')
|
||||
->with('success', 'Pomo created successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Pomo $pomo)
|
||||
{
|
||||
$this->authorize('view', $pomo);
|
||||
|
||||
return view('pomo.show', compact('pomo'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function edit(Pomo $pomo)
|
||||
{
|
||||
$this->authorize('view', $pomo);
|
||||
|
||||
$editing = true;
|
||||
|
||||
return view('pomo.create', compact('pomo', 'editing'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(UpdatePomoRequest $request, Pomo $pomo)
|
||||
{
|
||||
// $this->authorize('update', $pomo);
|
||||
|
||||
// Convert due_start and end to unix timestamp and save
|
||||
$pomo->pomo_start = strtotime($request->pomo_start);
|
||||
$pomo->pomo_end = strtotime($request->pomo_end);
|
||||
$pomo->notes = $request->notes;
|
||||
$pomo->save();
|
||||
|
||||
return redirect()->route('pomo.index')
|
||||
->with('success', 'Pomo updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function destroy(Pomo $pomo)
|
||||
{
|
||||
// Validate that the user is authorized to delete the pomo
|
||||
// $this->authorize('delete', $pomo);
|
||||
|
||||
$pomo->delete();
|
||||
|
||||
return redirect()->route('pomo.index')
|
||||
->with('success', 'Pomo deleted successfully.');
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Dashboard;
|
||||
|
||||
use App\Models\Pomo;
|
||||
use App\Models\User;
|
||||
use Livewire\Component;
|
||||
|
||||
class PomoCount extends Component
|
||||
{
|
||||
public int $ave_pomo_count = 0;
|
||||
|
||||
public function mount(){
|
||||
$user = User::find(auth()->user()->id);
|
||||
$todos = $user->todos()->map(function ($todo) {
|
||||
$todo = \App\Models\Todo::find($todo->id);
|
||||
$todo->pomos = Pomo::where('todo_id', $todo->id);
|
||||
return $todo;
|
||||
});
|
||||
|
||||
// Get the average pomo count per todo
|
||||
$ave_pomo_count = $todos->avg(function ($todo) {
|
||||
return $todo->pomos->count();
|
||||
});
|
||||
$this->ave_pomo_count = $ave_pomo_count;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard.pomo-count', [
|
||||
'ave_pomo_count' => $this->ave_pomo_count,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Dashboard;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\{
|
||||
User,
|
||||
Project,
|
||||
Todo
|
||||
};
|
||||
|
||||
class PomoTime extends Component
|
||||
{
|
||||
public int $ave_pomo_time = 0;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$user = User::find(auth()->user()->id);
|
||||
|
||||
// Get all pomos and calculate the average time spent per todo (due_end - due_start)/count/total pomos
|
||||
$pomos = $user->pomos();
|
||||
$pomos = $pomos->map(function ($pomo) {
|
||||
$pomo->todo = Todo::find($pomo->todo_id);
|
||||
$pomo->project = Project::find($pomo->todo->project_id);
|
||||
return $pomo;
|
||||
});
|
||||
|
||||
$total_pomos = $pomos->count();
|
||||
|
||||
$total_time = 0;
|
||||
|
||||
foreach ($pomos as $pomo) {
|
||||
$total_time += $pomo->pomo_end - $pomo->pomo_start;
|
||||
}
|
||||
|
||||
$this->ave_pomo_time = $total_time / $total_pomos;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard.pomo-time', [
|
||||
'ave_pomo_time' => $this->ave_pomo_time,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Pomo;
|
||||
|
||||
use App\Models\Pomo;
|
||||
use App\Models\Project;
|
||||
use App\Models\Todo;
|
||||
use App\Models\User;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Create extends Component
|
||||
{
|
||||
public $user;
|
||||
public $projects;
|
||||
public $incomplete_todos;
|
||||
public Pomo $pomo;
|
||||
public $editing = false;
|
||||
|
||||
public function mount(Pomo $pomo = null, $editing = false)
|
||||
{
|
||||
$this->user = User::find(auth()->id());
|
||||
$this->projects = $this->user->projects;
|
||||
$this->load_incomplete_todos(null);
|
||||
$this->pomo = $pomo;
|
||||
$this->editing = $editing;
|
||||
}
|
||||
|
||||
public function load_incomplete_todos($project_id = null, $editing = false)
|
||||
{
|
||||
$incomplete_todos = new Collection();
|
||||
|
||||
foreach ($this->projects as $project) {
|
||||
$todos = $project->todos()->where('completed_at', null);
|
||||
if ($project_id) {
|
||||
$todos = $todos->where('project_id', $project_id);
|
||||
}
|
||||
$incomplete_todos = $incomplete_todos->merge($todos->get());
|
||||
}
|
||||
$this->incomplete_todos = $incomplete_todos;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.pomo.create', [
|
||||
'projects' => $this->projects,
|
||||
'incomplete_todos' => $this->incomplete_todos,
|
||||
'pomo' => $this->pomo,
|
||||
'editing' => $this->editing,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Pomo;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\{
|
||||
Pomo,
|
||||
Project,
|
||||
User,
|
||||
Todo
|
||||
};
|
||||
|
||||
class Pomos extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
$user = User::find(auth()->id());
|
||||
$pomos = $user->pomos();
|
||||
|
||||
// Convert Pomos from Collection to class
|
||||
$pomos = $pomos->map(function ($pomo) {
|
||||
$pomo->todo = Todo::find($pomo->todo_id);
|
||||
$pomo->project = Project::find($pomo->todo->project_id);
|
||||
return $pomo;
|
||||
});
|
||||
|
||||
|
||||
return view('livewire.pomo.pomos', [
|
||||
'pomos' => $pomos,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StorePomoRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'todo_id' => 'required|exists:todos,id',
|
||||
'pomo_start' => 'required|date',
|
||||
'pomo_end' => 'required|date',
|
||||
'notes' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdatePomoRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\UuidTrait;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Pomo extends Model
|
||||
{
|
||||
use HasFactory, UuidTrait;
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
protected $table = 'pomos';
|
||||
|
||||
protected $fillable = [
|
||||
'todo_id',
|
||||
'notes',
|
||||
'pomo_start',
|
||||
'pomo_end',
|
||||
];
|
||||
|
||||
public function todo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Todo::class);
|
||||
}
|
||||
|
||||
}
|
|
@ -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\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
@ -54,4 +55,9 @@ class Todo extends Model
|
|||
return $this->hasOneThrough(Project::class, projectTodo::class, 'todo_id', 'id', 'id', 'project_id');
|
||||
}
|
||||
|
||||
|
||||
public function pomo(): HasMany
|
||||
{
|
||||
return $this->hasMany(Pomo::class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
|
@ -54,8 +56,28 @@ class User extends Authenticatable
|
|||
return $this->belongsToMany(Project::class, projectUser::class, 'user_id', 'project_id');
|
||||
}
|
||||
|
||||
public function todos(): HasManyThrough
|
||||
public function todos(): Collection
|
||||
{
|
||||
return $this->hasManyThrough(Todo::class, Project::class, 'owner_id', 'project_id');
|
||||
return DB::table('todos')
|
||||
->join('project_todo', 'todos.id', '=', 'project_todo.todo_id')
|
||||
->join('projects', 'project_todo.project_id', '=', 'projects.id')
|
||||
->join('project_user', 'projects.id', '=', 'project_user.project_id')
|
||||
->join('users', 'project_user.user_id', '=', 'users.id')
|
||||
->where('users.id', '=', $this->id)
|
||||
->select('todos.*')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function pomos(): Collection
|
||||
{
|
||||
return DB::table('pomos')
|
||||
->join('todos', 'pomos.todo_id', '=', 'todos.id')
|
||||
->join('project_todo', 'todos.id', '=', 'project_todo.todo_id')
|
||||
->join('projects', 'project_todo.project_id', '=', 'projects.id')
|
||||
->join('project_user', 'projects.id', '=', 'project_user.project_id')
|
||||
->join('users', 'project_user.user_id', '=', 'users.id')
|
||||
->where('users.id', '=', $this->id)
|
||||
->select('pomos.*')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Pomo;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class PomoPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Pomo $pomo): bool
|
||||
{
|
||||
if (!$user)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return $user->id === $pomo->todo->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, Pomo $pomo): bool
|
||||
{
|
||||
return $user->id === $pomo->todo->project->user->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Pomo $pomo): bool
|
||||
{
|
||||
// Check if the user is the owner of the pomo
|
||||
return $user->id === $pomo->todo->project->user->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Pomo $pomo): bool
|
||||
{
|
||||
return $user->id === $pomo->todo->project->user->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Pomo $pomo): bool
|
||||
{
|
||||
return $user->id === $pomo->todo->project->user->id;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,8 @@
|
|||
"guzzlehttp/guzzle": "^7.2",
|
||||
"laravel/framework": "^10.10",
|
||||
"laravel/sanctum": "^3.2",
|
||||
"laravel/tinker": "^2.8"
|
||||
"laravel/tinker": "^2.8",
|
||||
"livewire/livewire": "^2.12"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c0c3fcbb476f8d39c32adb863b4ed706",
|
||||
"content-hash": "a640a54c051b539eff089566e9e976ee",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
@ -2151,6 +2151,79 @@
|
|||
],
|
||||
"time": "2023-08-03T07:14:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "livewire/livewire",
|
||||
"version": "v2.12.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/livewire/livewire.git",
|
||||
"reference": "96a249f5ab51d8377817d802f91d1e440869c1d6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/livewire/livewire/zipball/96a249f5ab51d8377817d802f91d1e440869c1d6",
|
||||
"reference": "96a249f5ab51d8377817d802f91d1e440869c1d6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/database": "^7.0|^8.0|^9.0|^10.0",
|
||||
"illuminate/support": "^7.0|^8.0|^9.0|^10.0",
|
||||
"illuminate/validation": "^7.0|^8.0|^9.0|^10.0",
|
||||
"league/mime-type-detection": "^1.9",
|
||||
"php": "^7.2.5|^8.0",
|
||||
"symfony/http-kernel": "^5.0|^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"calebporzio/sushi": "^2.1",
|
||||
"laravel/framework": "^7.0|^8.0|^9.0|^10.0",
|
||||
"mockery/mockery": "^1.3.1",
|
||||
"orchestra/testbench": "^5.0|^6.0|^7.0|^8.0",
|
||||
"orchestra/testbench-dusk": "^5.2|^6.0|^7.0|^8.0",
|
||||
"phpunit/phpunit": "^8.4|^9.0",
|
||||
"psy/psysh": "@stable"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Livewire\\LivewireServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"Livewire": "Livewire\\Livewire"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Livewire\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Caleb Porzio",
|
||||
"email": "calebporzio@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A front-end framework for Laravel.",
|
||||
"support": {
|
||||
"issues": "https://github.com/livewire/livewire/issues",
|
||||
"source": "https://github.com/livewire/livewire/tree/v2.12.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/livewire",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-08-02T06:31:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.4.0",
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Pomo>
|
||||
*/
|
||||
class PomoFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'todo_id' => $this->faker->uuid(),
|
||||
'notes' => $this->faker->text(),
|
||||
'pomo_start' => $this->faker->unixTime(),
|
||||
'pomo_end' => $this->faker->unixTime(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
Schema::create('pomos', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('todo_id')->constrained('todos')->onDelete('cascade');
|
||||
$table->text('notes')->nullable();
|
||||
$table->integer('pomo_start')->nullable();
|
||||
$table->integer('pomo_end')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('pomos');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
<?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
|
||||
{
|
||||
Schema::table('pomos', function (Blueprint $table) {
|
||||
// Add created_at and updated_at columns to pomos table as unix timestamps
|
||||
$table->integer('created_at')->nullable();
|
||||
$table->integer('updated_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('pomos', function (Blueprint $table) {
|
||||
// Drop created_at and updated_at columns from pomos table
|
||||
$table->dropColumn('created_at');
|
||||
$table->dropColumn('updated_at');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -3,6 +3,7 @@
|
|||
namespace Database\Seeders;
|
||||
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
|
@ -14,13 +15,9 @@ class DatabaseSeeder extends Seeder
|
|||
{
|
||||
\App\Models\User::factory(3)
|
||||
->has(\App\Models\Project::factory()->count(3)
|
||||
->has(\App\Models\Todo::factory()->count(10)))
|
||||
->has(\App\Models\Todo::factory()->count(10)
|
||||
->has(\App\Models\Pomo::factory()->count(4))
|
||||
))
|
||||
->create();
|
||||
|
||||
|
||||
// \App\Models\User::factory()->create([
|
||||
// 'name' => 'Test User',
|
||||
// 'email' => 'test@example.com',
|
||||
// ]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class PomoSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -105,6 +105,9 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<livewire:dashboard.pomo-count />
|
||||
|
||||
<livewire:dashboard.pomo-time />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
|
@ -35,5 +36,6 @@
|
|||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -23,6 +23,13 @@
|
|||
{{ __('Projects') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
|
||||
<!-- Pomodoro List -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
|
||||
<x-nav-link :href="route('pomo.index')" :active="request()->routeIs('pomo.index')">
|
||||
{{ __('Pomodoro') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
|
@ -85,6 +92,10 @@
|
|||
<x-responsive-nav-link :href="route('project.index')" :active="request()->routeIs('project.index')">
|
||||
{{ __('Projects') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
<x-responsive-nav-link :href="route('pomo.index')" :active="request()->routeIs('pomo.index')">
|
||||
{{ __('Pomodoro') }}
|
||||
</x-responsive-nav-link>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
|
||||
<h3 class="text-xl font-semibold mb-4 text-blue-500 dark:text-blue-400">
|
||||
<!-- SVG for Pomo Average Cound -->
|
||||
<svg class="inline-block h-6 w-6 text-blue-500 dark:text-blue-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
Average Pomos per Project
|
||||
</h3>
|
||||
<p class="text-3xl font-bold text-gray-800 dark:text-gray-100">
|
||||
{{ $ave_pomo_count }}
|
||||
</p>
|
||||
</div>
|
|
@ -0,0 +1,19 @@
|
|||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
|
||||
<h3 class="text-xl font-semibold mb-4 text-blue-500 dark:text-blue-400">
|
||||
<!-- SVG for Pomo Average Cound -->
|
||||
<svg class="inline-block h-6 w-6 text-blue-500 dark:text-blue-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
Average Pomos time spent per Project
|
||||
</h3>
|
||||
<p class="text-3xl font-bold text-gray-800 dark:text-gray-100">
|
||||
{{ $ave_pomo_time }}
|
||||
</p>
|
||||
</div>
|
|
@ -0,0 +1,78 @@
|
|||
<div class="py-4" xmlns:livewire="http://www.w3.org/1999/html">
|
||||
<form method="POST" action="{{ $editing ? route('pomo.update', $pomo->id) : route('pomo.store') }}" id="pomo-form">
|
||||
@csrf
|
||||
@if($editing)
|
||||
@method('PUT')
|
||||
@endif
|
||||
|
||||
<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="todo_id" class="block mb-2 font-semibold">Todo</label>
|
||||
<select 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('todo_id') border-red-500 @enderror"
|
||||
name="todo_id" id="todo_id">
|
||||
<option selected value="{{ $editing ? $pomo->todo_id : old('todo_id') }}">{{ $editing ? $pomo->todo->title : 'Select a Todo' }}</option>
|
||||
@foreach($incomplete_todos as $todo)
|
||||
<option value="{{ $todo['id'] }}">{{ $todo['title'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('todo_id')
|
||||
<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">Notes</label>
|
||||
<textarea name="notes" id="notes" placeholder="Notes"
|
||||
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">{{ $editing ? $pomo->notes : old('notes') }}</textarea>
|
||||
@error('notes')
|
||||
<div class="text-red-500 mt-2 text-sm">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<!-- pomo_start and pomo_end -->
|
||||
<div class="flex flex-col sm:flex-row sm:space-x-2">
|
||||
<div class="mb-4 sm:mb-0">
|
||||
<label for="pomo_start" class="block mb-2 font-semibold">Start</label>
|
||||
<input type="datetime-local" name="pomo_start" id="pomo_start" placeholder="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('pomo_start') border-red-500 @enderror"
|
||||
value="{{ isset($pomo) ? date('Y-m-d\TH:i', $pomo->pomo_start) : old('pomo_start') }}">
|
||||
@error('pomo_start')
|
||||
<div class="text-red-500 mt-2 text-sm">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4 sm:mb-0">
|
||||
<label for="pomo_end" class="block mb-2 font-semibold">End</label>
|
||||
<input type="datetime-local" name="pomo_end" id="pomo_end" placeholder="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('pomo_end') border-red-500 @enderror"
|
||||
value="{{ isset($pomo) ? date('Y-m-d\TH:i', $pomo->pomo_end) : old('pomo_end') }}">
|
||||
@error('pomo_end')
|
||||
<div class="text-red-500 mt-2 text-sm">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<button type="submit"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded-lg">
|
||||
{{ $editing ? 'Update Pomo' : 'Create Pomo' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,62 @@
|
|||
<div class="flex justify-center mt-4">
|
||||
<table class="table-fixed w-3/4 border rounded-lg dark:bg-gray-800">
|
||||
<thead class="bg-blue-100">
|
||||
<tr>
|
||||
<th class="px-4 py-2">Task</th>
|
||||
<th class="px-4 py-2">Start</th>
|
||||
<th class="px-4 py-2">End</th>
|
||||
<th class="px-4 py-2">Duration (minutes)</th>
|
||||
<th class="px-4 py-2">Notes</th>
|
||||
<th class="px-4 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($pomos as $pomo)
|
||||
<tr class="hover:bg-blue-100 dark:hover:bg-gray-700">
|
||||
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
|
||||
<a href="{{ route('project.todo.edit', ['project' => $pomo->todo->project->id, 'todo' => $pomo->todo->id]) }}">
|
||||
{{ $pomo->todo->title }}
|
||||
</a>
|
||||
</td>
|
||||
<!-- Pomo Start and Pomo End -->
|
||||
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
|
||||
{{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_start)->format('Y-m-d H:i:s') }}
|
||||
</td>
|
||||
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
|
||||
{{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_end)->format('Y-m-d H:i:s') }}
|
||||
</td>
|
||||
<!-- Duration -->
|
||||
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
|
||||
{{
|
||||
\Carbon\Carbon::createFromTimestamp($pomo->pomo_start)->diffInMinutes(\Carbon\Carbon::createFromTimestamp($pomo->pomo_end))
|
||||
}}
|
||||
</td>
|
||||
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
|
||||
<!-- Truncate notes to 32 characters -->
|
||||
<div class="max-w-sm truncate">
|
||||
{{ $pomo->notes }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
|
||||
<!-- Edit and Delete form button groups -->
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-col">
|
||||
<a href="{{ route('pomo.edit', $pomo->id) }}"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Edit</a>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<form action="{{ route('pomo.destroy', $pomo->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __($editing ? 'Edit' : 'Create') }} {{ __('Pomo') }}
|
||||
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<livewire:pomo.create :pomo="$pomo" :editing="$editing">
|
||||
|
||||
</x-app-layout>
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Pomodoro Dashboard') }}
|
||||
<a href="{{ route('pomo.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded float-right">Create Pomo</a>
|
||||
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<livewire:pomo.pomos />
|
||||
|
||||
</x-app-layout>
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\PomoController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\ProjectController;
|
||||
use App\Http\Controllers\ProjectTodoController;
|
||||
|
@ -25,6 +26,13 @@ Route::get('/dashboard', [DashboardController::class, 'index'])
|
|||
->middleware(['auth', 'verified'])
|
||||
->name('dashboard');
|
||||
|
||||
Route::resource('pomo', PomoController::class)
|
||||
->middleware([
|
||||
'auth',
|
||||
'verified',
|
||||
'web'
|
||||
]);
|
||||
|
||||
Route::resource('project.todo', ProjectTodoController::class)
|
||||
->middleware([
|
||||
'auth',
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Project;
|
||||
|
||||
use App\Models\Pomo;
|
||||
use App\Models\Project;
|
||||
use App\Models\Todo;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PomoCRUDTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $user;
|
||||
private Project $project;
|
||||
private Todo $todo;
|
||||
private Pomo $pomo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
$this->user = $user;
|
||||
$this->assertAuthenticated();
|
||||
$this->todo = Todo::factory()->create();
|
||||
|
||||
}
|
||||
|
||||
public function test_user_can_create_pomo(): void
|
||||
{
|
||||
$now = now();
|
||||
$end = now()->addMinutes(25);
|
||||
// Create a pomo through POST and store it in the pomo property
|
||||
$response = $this->post(route('pomo.store'), [
|
||||
'todo_id' => $this->todo->id,
|
||||
'notes' => 'Test Notes',
|
||||
'pomo_start' => $now,
|
||||
'pomo_end' => $end,
|
||||
]);
|
||||
$response->assertRedirect(route('pomo.index'));
|
||||
$this->assertDatabaseHas('pomos', [
|
||||
'todo_id' => $this->todo->id,
|
||||
'notes' => 'Test Notes',
|
||||
'pomo_start' => strtotime($now),
|
||||
'pomo_end' => strtotime($end),
|
||||
]);
|
||||
|
||||
$this->pomo = Pomo::where('todo_id', $this->todo->id)->first();
|
||||
}
|
||||
|
||||
public function test_user_can_view_pomo(): void
|
||||
{
|
||||
$this->test_user_can_create_pomo();
|
||||
$this->assertDatabaseHas('pomos', [
|
||||
'todo_id' => $this->todo->id,
|
||||
'notes' => 'Test Notes',
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
public function test_user_can_update_pomo_with_authorsation(): void
|
||||
{
|
||||
$this->test_user_can_create_pomo();
|
||||
$now = now();
|
||||
$end = now()->addMinutes(25);
|
||||
$response = $this->put(route('pomo.update', $this->pomo->id), [
|
||||
'todo_id' => $this->todo->id,
|
||||
'notes' => 'Test Notes Updated',
|
||||
'pomo_start' => $now,
|
||||
'pomo_end' => $end,
|
||||
]);
|
||||
$response->assertRedirect(route('pomo.index'));
|
||||
$this->assertDatabaseHas('pomos', [
|
||||
'todo_id' => $this->todo->id,
|
||||
'notes' => 'Test Notes Updated',
|
||||
'pomo_start' => strtotime($now),
|
||||
'pomo_end' => strtotime($end),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_user_can_delete_pomo_with_authorsation(): void
|
||||
{
|
||||
$this->test_user_can_create_pomo();
|
||||
$response = $this->delete(route('pomo.destroy', $this->pomo->id));
|
||||
$response->assertRedirect(route('pomo.index'));
|
||||
$this->assertDatabaseMissing('pomos', [
|
||||
'todo_id' => $this->todo->id,
|
||||
'notes' => 'Test Notes',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue