reminder for todos

This commit is contained in:
Alexander Gabriel 2026-07-02 19:34:12 +00:00
parent f78740fbb0
commit ec6d7e2a80
9 changed files with 171 additions and 27 deletions

View File

@ -0,0 +1,37 @@
<?php
namespace App\Console\Commands;
use App\Mail\TodoMail;
use App\Models\Todo;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
#[Signature('app:send-todo-follow-up-reminders')]
#[Description('Send reminder emails for todos whose follow-up date has been reached')]
class SendTodoFollowUpReminders extends Command
{
/**
* Execute the console command.
*/
public function handle(): void
{
$todos = Todo::query()
->whereNull('done_date')
->whereNull('follow_up_notified_at')
->whereNotNull('follow_up')
->whereDate('follow_up', '<=', today())
->get();
foreach ($todos as $todo) {
foreach ($todo->getassignetusers() as $user) {
Mail::to($user)->send(new TodoMail($todo, 'Todo Wiedervorlage', 'Die Wiedervorlage für folgende Todo ist erreicht:'));
}
$todo->follow_up_notified_at = now();
$todo->saveQuietly();
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Filament\Actions;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Support\Icons\Heroicon;
use Illuminate\Database\Eloquent\Model;
class SetReminderAction extends ActionGroup
{
protected function setUp(): void
{
parent::setUp();
$this->label('Erinnerung')
->tooltip('Wiedervorlage setzen')
->icon(Heroicon::BellSnooze)
->iconButton()
->visible(function (Model $record) {
return $record->is_owner_or_assigned();
})
->actions([
Action::make('reminderIn1Day')
->label('in 1 Tag')
->action(fn (Model $record) => $record->update(['follow_up' => now()->addDay()])),
Action::make('reminderIn3Days')
->label('in 3 Tagen')
->action(fn (Model $record) => $record->update(['follow_up' => now()->addDays(3)])),
Action::make('reminderIn1Week')
->label('in 1 Woche')
->action(fn (Model $record) => $record->update(['follow_up' => now()->addWeek()])),
Action::make('reminderIn1Month')
->label('in 1 Monat')
->action(fn (Model $record) => $record->update(['follow_up' => now()->addMonth()])),
]);
}
}

View File

@ -4,6 +4,7 @@ namespace App\Filament\Resources\Todos\Pages;
use App\Filament\Actions\DoneTodoAction; use App\Filament\Actions\DoneTodoAction;
use App\Filament\Actions\ReopenTodoAction; use App\Filament\Actions\ReopenTodoAction;
use App\Filament\Actions\SetReminderAction;
use App\Filament\Resources\Todos\TodoResource; use App\Filament\Resources\Todos\TodoResource;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
@ -18,6 +19,7 @@ class EditTodo extends EditRecord
return [ return [
DoneTodoAction::make('done'), DoneTodoAction::make('done'),
ReopenTodoAction::make('reopen'), ReopenTodoAction::make('reopen'),
SetReminderAction::make([]),
DeleteAction::make()->iconButton()->icon(Heroicon::Trash), DeleteAction::make()->iconButton()->icon(Heroicon::Trash),
]; ];
} }

View File

@ -39,7 +39,7 @@ class TodoForm
Toggle::make('review')->label('Review')->hint('Todo geht nach Erledigung noch mal zum Ersteller'), Toggle::make('review')->label('Review')->hint('Todo geht nach Erledigung noch mal zum Ersteller'),
]), ]),
// DatePicker::make('follow_up')->label('Wiedervorlage/Erinnerung'), DatePicker::make('follow_up')->label('Wiedervorlage/Erinnerung')->hiddenOn(['create']),
DatePicker::make('done_date')->label('Erledigt am')->hiddenOn(['create']), DatePicker::make('done_date')->label('Erledigt am')->hiddenOn(['create']),
]); ]);
} }

View File

@ -4,14 +4,13 @@ namespace App\Filament\Resources\Todos\Tables;
use App\Filament\Actions\DoneTodoAction; use App\Filament\Actions\DoneTodoAction;
use App\Filament\Actions\ReopenTodoAction; use App\Filament\Actions\ReopenTodoAction;
use App\Filament\Actions\SetReminderAction;
use App\Models\Group; use App\Models\Group;
use App\Models\User; use App\Models\User;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup; use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter; use Filament\Tables\Filters\Filter;
@ -52,10 +51,10 @@ class TodosTable
->label('Erledigt am') ->label('Erledigt am')
->date() ->date()
->sortable(), ->sortable(),
// TextColumn::make('follow_up') TextColumn::make('follow_up')
// ->label('Wiedervorlage') ->label('Wiedervorlage')
// ->date() ->date()
// ->sortable(), ->sortable(),
IconColumn::make('review') IconColumn::make('review')
->label('Review') ->label('Review')
->boolean(), ->boolean(),
@ -66,6 +65,7 @@ class TodosTable
->label('nicht erledigt') ->label('nicht erledigt')
->query(function (Builder $query) { ->query(function (Builder $query) {
$query->whereNull('done_date'); $query->whereNull('done_date');
return $query; return $query;
}), }),
Filter::make('mine')->label('von mir oder für mich') Filter::make('mine')->label('von mir oder für mich')
@ -76,8 +76,24 @@ class TodosTable
->orWhere('user_id', filament()->auth()->user()->id) ->orWhere('user_id', filament()->auth()->user()->id)
->orWhere('todoable_type', User::class)->where('todoable_id', filament()->auth()->user()->id) ->orWhere('todoable_type', User::class)->where('todoable_id', filament()->auth()->user()->id)
->orwhere('todoable_type', Group::class)->whereIn('todoable_id', User::find(filament()->auth()->user()->id)->groups()->get()->pluck('id')); ->orwhere('todoable_type', Group::class)->whereIn('todoable_id', User::find(filament()->auth()->user()->id)->groups()->get()->pluck('id'));
return $query; return $query;
}); });
return $query;
}),
Filter::make('keineZukuenftigeWiedervorlage')
->label('ohne zukünftige Wiedervorlage')
->default()
->query(function (Builder $query) {
$query->where(function (Builder $query) {
$query
->whereNull('follow_up')
->orWhereDate('follow_up', '<=', today());
return $query;
});
return $query; return $query;
}), }),
@ -85,6 +101,7 @@ class TodosTable
->recordActions([ ->recordActions([
DoneTodoAction::make('done'), DoneTodoAction::make('done'),
ReopenTodoAction::make('reopen'), ReopenTodoAction::make('reopen'),
SetReminderAction::make([]),
EditAction::make()->iconButton(), EditAction::make()->iconButton(),
DeleteAction::make()->iconButton() DeleteAction::make()->iconButton()
->visible(function (Model $record) { ->visible(function (Model $record) {

View File

@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
#[Fillable('name', 'content', 'user_id', 'todoable_type', 'todoable_id', 'due_date', 'done_date', 'follow_up', 'review')] #[Fillable('name', 'content', 'user_id', 'todoable_type', 'todoable_id', 'due_date', 'done_date', 'follow_up', 'follow_up_notified_at', 'review')]
#[ObservedBy([TodoObserver::class])] #[ObservedBy([TodoObserver::class])]
class Todo extends Model class Todo extends Model
{ {
@ -23,15 +23,28 @@ class Todo extends Model
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
public function is_owner_or_assigned() { public function is_owner_or_assigned()
if(filament()->auth()->user()->id == $this->user_id) return true; {
if($this->todoable_type == Group::class && in_array(filament()->auth()->user()->id, $this->todoable()->first()->users()->get()->pluck('id')->toArray())) return true; if (filament()->auth()->user()->id == $this->user_id) {
if($this->todoable_type == User::class && $this->todoable()->first()->id == filament()->auth()->user()->id) return true; return true;
else return false; }
if ($this->todoable_type == Group::class && in_array(filament()->auth()->user()->id, $this->todoable()->first()->users()->get()->pluck('id')->toArray())) {
return true;
}
if ($this->todoable_type == User::class && $this->todoable()->first()->id == filament()->auth()->user()->id) {
return true;
} else {
return false;
}
} }
public function getassignetusers() { public function getassignetusers()
if($this->todoable_type == Group::class) return $this->todoable()->first()->users()->get(); {
if($this->todoable_type == User::class) return [$this->todoable()->first()]; if ($this->todoable_type == Group::class) {
return $this->todoable()->first()->users()->get();
}
if ($this->todoable_type == User::class) {
return [$this->todoable()->first()];
}
} }
} }

View File

@ -2,16 +2,10 @@
namespace App\Observers; namespace App\Observers;
use App\Mail\TodoAssigned;
use App\Mail\TodoCreated;
use App\Mail\TodoMail; use App\Mail\TodoMail;
use App\Mail\TodoNeedsReview;
use App\Mail\TodoReopened;
use App\Models\Group;
use App\Models\Todo; use App\Models\Todo;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class TodoObserver class TodoObserver
{ {
@ -20,12 +14,13 @@ class TodoObserver
*/ */
public function created(Todo $todo): void public function created(Todo $todo): void
{ {
if($todo->todoable_type == null) return; if ($todo->todoable_type == null) {
// if($todo->todoable_type == User::class && $todo->user_id == $todo->todoable_id) return; return;
if($todo->todoable_type == User::class) {
Mail::to(User::find($todo->todoable()->first()))->send(new TodoMail($todo, 'Neue Todo', 'Es gibt eine neue Todo für dich:'));
} }
else { // if($todo->todoable_type == User::class && $todo->user_id == $todo->todoable_id) return;
if ($todo->todoable_type == User::class) {
Mail::to(User::find($todo->todoable()->first()))->send(new TodoMail($todo, 'Neue Todo', 'Es gibt eine neue Todo für dich:'));
} else {
$users = $todo->todoable()->first()->users()->get(); $users = $todo->todoable()->first()->users()->get();
foreach ($users as $user) { foreach ($users as $user) {
Mail::to($user)->send(new TodoMail($todo, 'Neue Todo', 'Es gibt eine neue Todo für dich:')); Mail::to($user)->send(new TodoMail($todo, 'Neue Todo', 'Es gibt eine neue Todo für dich:'));
@ -33,6 +28,16 @@ class TodoObserver
} }
} }
/**
* Handle the Todo "updating" event.
*/
public function updating(Todo $todo): void
{
if ($todo->isDirty('follow_up')) {
$todo->follow_up_notified_at = null;
}
}
/** /**
* Handle the Todo "updated" event. * Handle the Todo "updated" event.
*/ */

View File

@ -1,5 +1,6 @@
<?php <?php
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
@ -15,4 +16,7 @@ return Application::configure(basePath: dirname(__DIR__))
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //
})
->withSchedule(function (Schedule $schedule): void {
$schedule->command('app:send-todo-follow-up-reminders')->dailyAt('07:00');
})->create(); })->create();

View File

@ -0,0 +1,28 @@
<?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('todos', function (Blueprint $table) {
$table->timestamp('follow_up_notified_at')->nullable()->after('follow_up');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('todos', function (Blueprint $table) {
$table->dropColumn('follow_up_notified_at');
});
}
};