mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-17 10:13:16 +01:00
Add still-running email preference
This commit is contained in:
committed by
Gregor Vostrak
parent
db9ca51fc4
commit
593372bae5
@@ -51,7 +51,8 @@ class TimeEntrySendStillRunningMailsCommand extends Command
|
|||||||
])
|
])
|
||||||
->whereHas('user', function (Builder $query): void {
|
->whereHas('user', function (Builder $query): void {
|
||||||
/** @var Builder<User> $query */
|
/** @var Builder<User> $query */
|
||||||
$query->where('is_placeholder', '=', false);
|
$query->where('is_placeholder', '=', false)
|
||||||
|
->where('send_time_entry_still_running_email', '=', true);
|
||||||
})
|
})
|
||||||
->orderBy('created_at', 'asc')
|
->orderBy('created_at', 'asc')
|
||||||
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails): void {
|
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails): void {
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ class UserController extends Controller
|
|||||||
$user->week_start = $request->getWeekStart();
|
$user->week_start = $request->getWeekStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->getSendTimeEntryStillRunningEmail() !== null) {
|
||||||
|
$user->send_time_entry_still_running_email = $request->getSendTimeEntryStillRunningEmail();
|
||||||
|
}
|
||||||
|
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
if ($emailToVerify !== null) {
|
if ($emailToVerify !== null) {
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ class UserUpdateRequest extends BaseFormRequest
|
|||||||
'week_start' => [
|
'week_start' => [
|
||||||
Rule::enum(Weekday::class),
|
Rule::enum(Weekday::class),
|
||||||
],
|
],
|
||||||
|
'send_time_entry_still_running_email' => [
|
||||||
|
'boolean',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +84,13 @@ class UserUpdateRequest extends BaseFormRequest
|
|||||||
return $this->has('week_start') ? Weekday::from($this->input('week_start')) : null;
|
return $this->has('week_start') ? Weekday::from($this->input('week_start')) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSendTimeEntryStillRunningEmail(): ?bool
|
||||||
|
{
|
||||||
|
return $this->has('send_time_entry_still_running_email')
|
||||||
|
? $this->boolean('send_time_entry_still_running_email')
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
public function hasPhotoKey(): bool
|
public function hasPhotoKey(): bool
|
||||||
{
|
{
|
||||||
return $this->has('photo');
|
return $this->has('photo');
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class UserResource extends BaseResource
|
|||||||
'timezone' => $this->resource->timezone,
|
'timezone' => $this->resource->timezone,
|
||||||
/** @var Weekday $week_start Starting day of the week */
|
/** @var Weekday $week_start Starting day of the week */
|
||||||
'week_start' => $this->resource->week_start->value,
|
'week_start' => $this->resource->week_start->value,
|
||||||
|
/** @var bool $send_time_entry_still_running_email Whether to email the user when a time entry has been running for more than 8 hours */
|
||||||
|
'send_time_entry_still_running_email' => $this->resource->send_time_entry_still_running_email,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
|||||||
* @property string $timezone
|
* @property string $timezone
|
||||||
* @property bool $is_placeholder
|
* @property bool $is_placeholder
|
||||||
* @property Weekday $week_start
|
* @property Weekday $week_start
|
||||||
|
* @property bool $send_time_entry_still_running_email
|
||||||
* @property string|null $profile_photo_path
|
* @property string|null $profile_photo_path
|
||||||
* @property-read Organization|null $currentOrganization
|
* @property-read Organization|null $currentOrganization
|
||||||
* @property-read string $profile_photo_url
|
* @property-read string $profile_photo_url
|
||||||
@@ -108,6 +109,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
'is_admin' => 'boolean',
|
'is_admin' => 'boolean',
|
||||||
'is_placeholder' => 'boolean',
|
'is_placeholder' => 'boolean',
|
||||||
'week_start' => Weekday::class,
|
'week_start' => Weekday::class,
|
||||||
|
'send_time_entry_still_running_email' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,6 +119,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
|||||||
*/
|
*/
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
'week_start' => Weekday::Monday,
|
'week_start' => Weekday::Monday,
|
||||||
|
'send_time_entry_still_running_email' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table): void {
|
||||||
|
$table->boolean('send_time_entry_still_running_email')->default(true)->after('week_start');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('send_time_entry_still_running_email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import { Button } from '@/packages/ui/src/Buttons';
|
|||||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
|
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
|
||||||
|
import { Checkbox } from '@/packages/ui/src';
|
||||||
import {
|
import {
|
||||||
useResendUserEmailVerificationMutation,
|
useResendUserEmailVerificationMutation,
|
||||||
useResetUserPendingEmailMutation,
|
useResetUserPendingEmailMutation,
|
||||||
@@ -26,6 +27,7 @@ const name = ref('');
|
|||||||
const email = ref('');
|
const email = ref('');
|
||||||
const timezone = ref('');
|
const timezone = ref('');
|
||||||
const weekStart = ref('');
|
const weekStart = ref('');
|
||||||
|
const sendTimeEntryStillRunningEmail = ref(true);
|
||||||
|
|
||||||
const photoBase64 = ref<string | null>(null);
|
const photoBase64 = ref<string | null>(null);
|
||||||
const photoPreview = ref<string | null>(null);
|
const photoPreview = ref<string | null>(null);
|
||||||
@@ -40,6 +42,7 @@ function seedForm(u: User) {
|
|||||||
email.value = u.email;
|
email.value = u.email;
|
||||||
timezone.value = u.timezone;
|
timezone.value = u.timezone;
|
||||||
weekStart.value = u.week_start;
|
weekStart.value = u.week_start;
|
||||||
|
sendTimeEntryStillRunningEmail.value = u.send_time_entry_still_running_email;
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -78,6 +81,11 @@ function buildPayload(): UpdateUserBody {
|
|||||||
if (weekStart.value !== user.value.week_start) {
|
if (weekStart.value !== user.value.week_start) {
|
||||||
body.week_start = weekStart.value as UpdateUserBody['week_start'];
|
body.week_start = weekStart.value as UpdateUserBody['week_start'];
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
sendTimeEntryStillRunningEmail.value !== user.value.send_time_entry_still_running_email
|
||||||
|
) {
|
||||||
|
body.send_time_entry_still_running_email = sendTimeEntryStillRunningEmail.value;
|
||||||
|
}
|
||||||
if (photoBase64.value !== null) body.photo = photoBase64.value;
|
if (photoBase64.value !== null) body.photo = photoBase64.value;
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
@@ -330,6 +338,21 @@ const page = usePage<{
|
|||||||
</select>
|
</select>
|
||||||
<FieldError v-if="fieldErrors.week_start">{{ fieldErrors.week_start }}</FieldError>
|
<FieldError v-if="fieldErrors.week_start">{{ fieldErrors.week_start }}</FieldError>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<div class="col-span-6 sm:col-span-4">
|
||||||
|
<Field orientation="horizontal">
|
||||||
|
<Checkbox
|
||||||
|
id="send_time_entry_still_running_email"
|
||||||
|
v-model:checked="sendTimeEntryStillRunningEmail"
|
||||||
|
:disabled="!isUserLoaded" />
|
||||||
|
<FieldLabel for="send_time_entry_still_running_email">
|
||||||
|
Email me when a time entry has been running for more than 8 hours
|
||||||
|
</FieldLabel>
|
||||||
|
</Field>
|
||||||
|
<FieldError v-if="fieldErrors.send_time_entry_still_running_email">
|
||||||
|
{{ fieldErrors.send_time_entry_still_running_email }}
|
||||||
|
</FieldError>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
|||||||
@@ -705,6 +705,7 @@ const UserResource = z
|
|||||||
profile_photo_url: z.string(),
|
profile_photo_url: z.string(),
|
||||||
timezone: z.string(),
|
timezone: z.string(),
|
||||||
week_start: Weekday,
|
week_start: Weekday,
|
||||||
|
send_time_entry_still_running_email: z.boolean(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const UserUpdateRequest = z
|
const UserUpdateRequest = z
|
||||||
@@ -714,6 +715,7 @@ const UserUpdateRequest = z
|
|||||||
photo: z.union([z.string(), z.null()]),
|
photo: z.union([z.string(), z.null()]),
|
||||||
timezone: z.string(),
|
timezone: z.string(),
|
||||||
week_start: Weekday,
|
week_start: Weekday,
|
||||||
|
send_time_entry_still_running_email: z.boolean(),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|||||||
1
resources/js/types/models.d.ts
vendored
1
resources/js/types/models.d.ts
vendored
@@ -76,6 +76,7 @@ export interface User {
|
|||||||
two_factor_confirmed_at: string | null;
|
two_factor_confirmed_at: string | null;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
week_start: string;
|
week_start: string;
|
||||||
|
send_time_entry_still_running_email: boolean;
|
||||||
profile_photo_url: string;
|
profile_photo_url: string;
|
||||||
organizations: Organization[];
|
organizations: Organization[];
|
||||||
clients: Client[];
|
clients: Client[];
|
||||||
|
|||||||
@@ -159,4 +159,26 @@ class TimeEntrySendStillRunningMailsCommandTest extends TestCaseWithDatabase
|
|||||||
$this->assertSame("Sending still running time entry emails...\n".
|
$this->assertSame("Sending still running time entry emails...\n".
|
||||||
"Finished sending 0 still running time entry emails...\n", $output);
|
"Finished sending 0 still running time entry emails...\n", $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_does_not_send_emails_to_users_who_disabled_them(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$user = $this->createUserWithPermission();
|
||||||
|
$user->user->send_time_entry_still_running_email = false;
|
||||||
|
$user->user->save();
|
||||||
|
$timeEntry = 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::assertNothingOutgoing();
|
||||||
|
$this->assertNull($timeEntry->fresh()->still_active_email_sent_at);
|
||||||
|
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||||
|
$this->assertSame("Sending still running time entry emails...\n".
|
||||||
|
"Finished sending 0 still running time entry emails...\n", Artisan::output());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
|||||||
'profile_photo_url' => $data->user->profile_photo_url,
|
'profile_photo_url' => $data->user->profile_photo_url,
|
||||||
'timezone' => $data->user->timezone,
|
'timezone' => $data->user->timezone,
|
||||||
'week_start' => $data->user->week_start->value,
|
'week_start' => $data->user->week_start->value,
|
||||||
|
'send_time_entry_still_running_email' => true,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -131,7 +132,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$response->assertJsonValidationErrors('organization_id');
|
$response->assertJsonValidationErrors('organization_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_update_changes_user_name_timezone_and_week_start(): void
|
public function test_update_changes_user_profile_settings(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$data = $this->createUserWithPermission();
|
$data = $this->createUserWithPermission();
|
||||||
@@ -142,6 +143,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
|||||||
'name' => 'Updated Name',
|
'name' => 'Updated Name',
|
||||||
'timezone' => 'America/New_York',
|
'timezone' => 'America/New_York',
|
||||||
'week_start' => Weekday::Sunday->value,
|
'week_start' => Weekday::Sunday->value,
|
||||||
|
'send_time_entry_still_running_email' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -152,6 +154,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
|||||||
'name' => 'Updated Name',
|
'name' => 'Updated Name',
|
||||||
'timezone' => 'America/New_York',
|
'timezone' => 'America/New_York',
|
||||||
'week_start' => Weekday::Sunday->value,
|
'week_start' => Weekday::Sunday->value,
|
||||||
|
'send_time_entry_still_running_email' => false,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -159,6 +162,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$this->assertSame('Updated Name', $user->name);
|
$this->assertSame('Updated Name', $user->name);
|
||||||
$this->assertSame('America/New_York', $user->timezone);
|
$this->assertSame('America/New_York', $user->timezone);
|
||||||
$this->assertSame(Weekday::Sunday, $user->week_start);
|
$this->assertSame(Weekday::Sunday, $user->week_start);
|
||||||
|
$this->assertFalse($user->send_time_entry_still_running_email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_update_does_not_change_user_fields_that_are_not_given(): void
|
public function test_update_does_not_change_user_fields_that_are_not_given(): void
|
||||||
@@ -503,6 +507,22 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$response->assertJsonValidationErrors(['week_start']);
|
$response->assertJsonValidationErrors(['week_start']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_update_fails_if_send_time_entry_still_running_email_is_not_boolean(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->putJson(route('api.v1.users.update', $data->user->getKey()), [
|
||||||
|
'send_time_entry_still_running_email' => 'not-a-boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertUnprocessable();
|
||||||
|
$response->assertJsonValidationErrors(['send_time_entry_still_running_email']);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_update_fails_if_photo_is_not_a_string(): void
|
public function test_update_fails_if_photo_is_not_a_string(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
Reference in New Issue
Block a user