feature(Pomo Feature):

Added Pomo Tables + Features + Test Cases
Added Livewire support
This commit is contained in:
devoalda 2023-08-08 15:47:47 +08:00
parent 0603b56602
commit 9df9c8d2f2
29 changed files with 975 additions and 11 deletions

View File

@ -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.');
}
}

View File

@ -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,
]);
}
}

View File

@ -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,
]);
}
}

View File

@ -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,
]);
}
}

View File

@ -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,
]);
}
}

View File

@ -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',
];
}
}

View File

@ -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 [
//
];
}
}

29
app/Models/Pomo.php Normal file
View File

@ -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);
}
}

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\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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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",

75
composer.lock generated
View File

@ -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",

View File

@ -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(),
];
}
}

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
{
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');
}
};

View File

@ -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');
});
}
};

View File

@ -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',
// ]);
}
}

View File

@ -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
{
//
}
}

View File

@ -105,6 +105,9 @@
</p>
</div>
<livewire:dashboard.pomo-count />
<livewire:dashboard.pomo-time />
</div>
</div>

View File

@ -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>

View File

@ -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 -->

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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',

View File

@ -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',
]);
}
}