mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
Added mail to inform users about still running time entries
This commit is contained in:
committed by
Constantin Graf
parent
855db81104
commit
8db0a7d25e
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\TimeEntry;
|
||||
|
||||
use App\Mail\TimeEntryStillRunningMail;
|
||||
use App\Models\TimeEntry;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class TimeEntrySendStillRunningMailsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'time-entry:send-still-running-mails '.
|
||||
' { --dry-run : Do not actually send emails or save anything to the database, just output what would happen }';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Sends emails to users who have running time entries for more than 8 hours.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->comment('Sending still running time entry emails...');
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
if ($dryRun) {
|
||||
$this->comment('Running in dry-run mode. No emails will be sent and nothing will be saved to the database.');
|
||||
}
|
||||
|
||||
$sentMails = 0;
|
||||
TimeEntry::query()
|
||||
->whereNull('end')
|
||||
->where('start', '<', now()->subHours(8))
|
||||
->whereNull('still_active_email_sent_at')
|
||||
->with([
|
||||
'user',
|
||||
])
|
||||
->orderBy('created_at', 'asc')
|
||||
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails) {
|
||||
/** @var Collection<int, TimeEntry> $timeEntries */
|
||||
foreach ($timeEntries as $timeEntry) {
|
||||
$user = $timeEntry->user;
|
||||
$this->info('Start sending email to user "'.$user->email.'" ('.$user->getKey().') for time entry '.$timeEntry->getKey());
|
||||
$sentMails++;
|
||||
if (! $dryRun) {
|
||||
Mail::to($user->email)
|
||||
->queue(new TimeEntryStillRunningMail($timeEntry, $user));
|
||||
$timeEntry->still_active_email_sent_at = Carbon::now();
|
||||
$timeEntry->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->comment('Finished sending '.$sentMails.' still running time entry emails...');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
$schedule->command('time-entry:send-still-running-mails')
|
||||
->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails'))
|
||||
->everyTenMinutes();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
43
app/Mail/TimeEntryStillRunningMail.php
Normal file
43
app/Mail/TimeEntryStillRunningMail.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class TimeEntryStillRunningMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public TimeEntry $timeEntry;
|
||||
|
||||
public User $user;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(TimeEntry $timeEntry, User $user)
|
||||
{
|
||||
$this->timeEntry = $timeEntry;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*/
|
||||
public function build(): self
|
||||
{
|
||||
return $this->markdown('emails.time-entry-still-running', [
|
||||
'dashboardUrl' => URL::route('dashboard'),
|
||||
])
|
||||
->subject(__('Your Time Tracker is still running!'));
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ use Korridor\LaravelComputedAttributes\ComputedAttributes;
|
||||
* @property string $user_id
|
||||
* @property string $member_id
|
||||
* @property bool $is_imported
|
||||
* @property Carbon|null $still_active_email_sent_at
|
||||
* @property-read User $user
|
||||
* @property-read Member $member
|
||||
* @property string $organization_id
|
||||
@@ -59,6 +60,7 @@ class TimeEntry extends Model
|
||||
'tags' => 'array',
|
||||
'billable_rate' => 'int',
|
||||
'is_imported' => 'bool',
|
||||
'still_active_email_sent_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,6 +63,9 @@ class AdminPanelProvider extends PanelProvider
|
||||
NavigationGroup::make()
|
||||
->label('Users')
|
||||
->collapsed(),
|
||||
NavigationGroup::make()
|
||||
->label('System')
|
||||
->collapsed(),
|
||||
])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
|
||||
10
config/scheduling.php
Normal file
10
config/scheduling.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'tasks' => [
|
||||
'time_entry_send_still_running_mails' => (bool) env('SCHEDULING_TASK_TIME_ENTRY_SEND_STILL_RUNNING_MAILS', true),
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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('time_entries', function (Blueprint $table) {
|
||||
$table->dateTime('still_active_email_sent_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('time_entries', function (Blueprint $table) {
|
||||
$table->dropColumn('still_active_email_sent_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
14
resources/views/emails/time-entry-still-running.blade.php
Normal file
14
resources/views/emails/time-entry-still-running.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
@component('mail::message')
|
||||
@if(empty($timeEntry->description))
|
||||
{{ __('Your currently running time entry is now running for more than 8 hours!') }}
|
||||
@else
|
||||
{{ __('Your currently running time entry ":description" is now running for more than 8 hours!', ['description' => $timeEntry->description]) }}
|
||||
@endif
|
||||
|
||||
{{ __('If you forgot to stop the Time Tracker you do that in solidtime:') }}
|
||||
|
||||
@component('mail::button', ['url' => $dashboardUrl])
|
||||
{{ __('Go to solidtime') }}
|
||||
@endcomponent
|
||||
|
||||
@endcomponent
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Console\Commands\TimeEntry;
|
||||
|
||||
use App\Console\Commands\TimeEntry\TimeEntrySendStillRunningMailsCommand;
|
||||
use App\Mail\TimeEntryStillRunningMail;
|
||||
use App\Models\TimeEntry;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(TimeEntrySendStillRunningMailsCommand::class)]
|
||||
#[UsesClass(TimeEntrySendStillRunningMailsCommand::class)]
|
||||
class TimeEntrySendStillRunningMailsCommandTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_sends_mails_for_still_running_time_entries(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$timeEntryRunningLongerThanThreshold = TimeEntry::factory()->forMember($user->member)->create([
|
||||
'start' => Carbon::now()->subHours(8)->subSecond(),
|
||||
'end' => null,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails');
|
||||
|
||||
// Assert
|
||||
Mail::assertQueued(TimeEntryStillRunningMail::class, function ($mail) use ($user, $timeEntryRunningLongerThanThreshold) {
|
||||
return $mail->hasTo($user->user->email) &&
|
||||
$mail->timeEntry->is($timeEntryRunningLongerThanThreshold) &&
|
||||
$mail->user->is($user->user);
|
||||
});
|
||||
$timeEntryRunningLongerThanThreshold->refresh();
|
||||
$this->assertNotNull($timeEntryRunningLongerThanThreshold->still_active_email_sent_at);
|
||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||
$output = Artisan::output();
|
||||
$this->assertSame("Sending still running time entry emails...\n".
|
||||
'Start sending email to user "'.$user->user->email.'" ('.$user->user->getKey().') for time entry '.$timeEntryRunningLongerThanThreshold->getKey()."\n".
|
||||
"Finished sending 1 still running time entry emails...\n", $output);
|
||||
|
||||
}
|
||||
|
||||
public function test_does_not_send_emails_for_not_running_time_entries(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$timeEntry = TimeEntry::factory()->forMember($user->member)->create([
|
||||
'start' => Carbon::now()->subHours(8)->subSecond(),
|
||||
'end' => Carbon::now(),
|
||||
]);
|
||||
|
||||
// Act
|
||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails');
|
||||
|
||||
// Assert
|
||||
Mail::assertNothingOutgoing();
|
||||
$timeEntry->refresh();
|
||||
$this->assertNull($timeEntry->still_active_email_sent_at);
|
||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||
$output = Artisan::output();
|
||||
$this->assertSame("Sending still running time entry emails...\n".
|
||||
"Finished sending 0 still running time entry emails...\n", $output);
|
||||
}
|
||||
|
||||
public function test_does_not_send_emails_for_running_time_entries_that_are_short_than_the_threshold(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$timeEntry = TimeEntry::factory()->forMember($user->member)->create([
|
||||
'start' => Carbon::now()->subHours(8)->addMinute(),
|
||||
'end' => null,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails');
|
||||
|
||||
// Assert
|
||||
Mail::assertNothingOutgoing();
|
||||
$timeEntry->refresh();
|
||||
$this->assertNull($timeEntry->still_active_email_sent_at);
|
||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||
$output = Artisan::output();
|
||||
$this->assertSame("Sending still running time entry emails...\n".
|
||||
"Finished sending 0 still running time entry emails...\n", $output);
|
||||
}
|
||||
|
||||
public function test_does_not_send_emails_for_running_time_entries_that_are_longer_than_the_threshold_but_already_received_the_email(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$timeEntry = TimeEntry::factory()->forMember($user->member)->create([
|
||||
'start' => Carbon::now()->subHours(8)->subMinute(),
|
||||
'end' => null,
|
||||
'still_active_email_sent_at' => Carbon::now()->subMinute(),
|
||||
]);
|
||||
|
||||
// Act
|
||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails');
|
||||
|
||||
// Assert
|
||||
Mail::assertNothingOutgoing();
|
||||
$timeEntry->refresh();
|
||||
$this->assertNotNull($timeEntry->still_active_email_sent_at);
|
||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||
$output = Artisan::output();
|
||||
$this->assertSame("Sending still running time entry emails...\n".
|
||||
"Finished sending 0 still running time entry emails...\n", $output);
|
||||
}
|
||||
|
||||
public function test_dry_run_option_does_not_send_mails_but_outputs_what_would_happen(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$timeEntryRunningLongerThanThreshold = TimeEntry::factory()->forMember($user->member)->create([
|
||||
'start' => Carbon::now()->subHours(8)->subSecond(),
|
||||
'end' => null,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails --dry-run');
|
||||
|
||||
// Assert
|
||||
Mail::assertNothingOutgoing();
|
||||
$timeEntryRunningLongerThanThreshold->refresh();
|
||||
$this->assertNull($timeEntryRunningLongerThanThreshold->still_active_email_sent_at);
|
||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||
$output = Artisan::output();
|
||||
$this->assertSame("Sending still running time entry emails...\n".
|
||||
"Running in dry-run mode. No emails will be sent and nothing will be saved to the database.\n".
|
||||
'Start sending email to user "'.$user->user->email.'" ('.$user->user->getKey().') for time entry '.$timeEntryRunningLongerThanThreshold->getKey()."\n".
|
||||
"Finished sending 1 still running time entry emails...\n", $output);
|
||||
}
|
||||
}
|
||||
32
tests/Unit/Mail/TimeEntryStillRunningMailTest.php
Normal file
32
tests/Unit/Mail/TimeEntryStillRunningMailTest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Mail;
|
||||
|
||||
use App\Mail\TimeEntryStillRunningMail;
|
||||
use App\Models\TimeEntry;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(TimeEntryStillRunningMail::class)]
|
||||
#[UsesClass(TimeEntryStillRunningMail::class)]
|
||||
class TimeEntryStillRunningMailTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_mail_renders_content_correctly(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$timeEntry = TimeEntry::factory()->create([
|
||||
'description' => 'TEST 123',
|
||||
]);
|
||||
$mail = new TimeEntryStillRunningMail($timeEntry, $user->user);
|
||||
|
||||
// Act
|
||||
$rendered = $mail->render();
|
||||
|
||||
// Assert
|
||||
$this->assertStringContainsString('Your currently running time entry "TEST 123"', $rendered);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user