From 01659923facb4bba3b154898aade4b194d16fdae Mon Sep 17 00:00:00 2001 From: myrmidex Date: Tue, 18 Aug 2026 22:01:59 +0200 Subject: [PATCH] 51 - Indicate eating out when skipping a meal --- app/Livewire/Schedule/ScheduleCalendar.php | 48 +++++++- app/Models/ScheduledUserDish.php | 2 + ..._reason_to_scheduled_user_dishes_table.php | 22 ++++ .../schedule/schedule-calendar.blade.php | 34 +++++- .../SkipScheduledUserDishForDateAction.php | 3 +- .../Feature/Schedule/ScheduleCalendarTest.php | 107 ++++++++++++++++++ ...SkipScheduledUserDishForDateActionTest.php | 35 ++++++ 7 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 database/migrations/2026_08_18_000000_add_skip_reason_to_scheduled_user_dishes_table.php create mode 100644 tests/Feature/Schedule/ScheduleCalendarTest.php diff --git a/app/Livewire/Schedule/ScheduleCalendar.php b/app/Livewire/Schedule/ScheduleCalendar.php index 19e100f..63db046 100644 --- a/app/Livewire/Schedule/ScheduleCalendar.php +++ b/app/Livewire/Schedule/ScheduleCalendar.php @@ -30,6 +30,15 @@ class ScheduleCalendar extends Component public $regenerateUserId = null; + // Skip modal + public bool $showSkipModal = false; + + public ?string $skipDate = null; + + public ?int $skipUserId = null; + + public string $skipReason = ''; + // Edit dish modal public $showEditDishModal = false; @@ -145,26 +154,52 @@ public function confirmRegenerate(): void } public function skipDay($date, $userId): void + { + if (! $this->authorizeUser($userId)) { + session()->flash('error', 'Unauthorized action.'); + + return; + } + + $this->skipDate = $date; + $this->skipUserId = $userId; + $this->skipReason = ''; + $this->showSkipModal = true; + } + + public function confirmSkip(): void { try { - if (! $this->authorizeUser($userId)) { + if (! $this->authorizeUser((int) $this->skipUserId)) { session()->flash('error', 'Unauthorized action.'); return; } + $this->validate([ + 'skipReason' => ['nullable', 'string', 'max:255'], + ]); + + $reason = trim($this->skipReason) ?: null; + $action = new SkipScheduledUserDishForDateAction; $action->execute( auth()->user(), - Carbon::parse($date), - $userId + Carbon::parse($this->skipDate), + $this->skipUserId, + $reason ); + $this->showSkipModal = false; + $this->skipDate = null; + $this->skipUserId = null; + $this->skipReason = ''; + $this->loadCalendar(); session()->flash('success', 'Day skipped successfully!'); } catch (Exception $e) { - Log::error('Skip day failed', ['exception' => $e, 'date' => $date, 'userId' => $userId]); + Log::error('Skip day failed', ['exception' => $e, 'date' => $this->skipDate, 'userId' => $this->skipUserId]); session()->flash('error', 'Unable to skip day. Please try again.'); } } @@ -181,6 +216,10 @@ public function cancel(): void $this->showRegenerateModal = false; $this->regenerateDate = null; $this->regenerateUserId = null; + $this->showSkipModal = false; + $this->skipDate = null; + $this->skipUserId = null; + $this->skipReason = ''; $this->showEditDishModal = false; $this->editDate = null; $this->editUserId = null; @@ -436,6 +475,7 @@ public function saveDish(): void [ 'user_dish_id' => $userDish->id, 'is_skipped' => false, + 'skip_reason' => null, ] ); diff --git a/app/Models/ScheduledUserDish.php b/app/Models/ScheduledUserDish.php index 8334e2a..dcbdc46 100644 --- a/app/Models/ScheduledUserDish.php +++ b/app/Models/ScheduledUserDish.php @@ -17,6 +17,7 @@ * @property int $user_dish_id * @property UserDish $userDish * @property bool $is_skipped + * @property string|null $skip_reason * * @method static create(array $array) * @method static ScheduledUserDishFactory factory($count = null, $state = []) @@ -31,6 +32,7 @@ class ScheduledUserDish extends Model 'user_id', 'user_dish_id', 'is_skipped', + 'skip_reason', ]; protected $casts = [ diff --git a/database/migrations/2026_08_18_000000_add_skip_reason_to_scheduled_user_dishes_table.php b/database/migrations/2026_08_18_000000_add_skip_reason_to_scheduled_user_dishes_table.php new file mode 100644 index 0000000..492c079 --- /dev/null +++ b/database/migrations/2026_08_18_000000_add_skip_reason_to_scheduled_user_dishes_table.php @@ -0,0 +1,22 @@ +string('skip_reason')->nullable()->after('is_skipped'); + }); + } + + public function down(): void + { + Schema::table('scheduled_user_dishes', function (Blueprint $table) { + $table->dropColumn('skip_reason'); + }); + } +}; diff --git a/resources/views/livewire/schedule/schedule-calendar.blade.php b/resources/views/livewire/schedule/schedule-calendar.blade.php index 83fad1f..0c9f7dc 100644 --- a/resources/views/livewire/schedule/schedule-calendar.blade.php +++ b/resources/views/livewire/schedule/schedule-calendar.blade.php @@ -68,7 +68,7 @@ class="w-5 h-5 bg-gray-600 hover:bg-primary text-gray-300 hover:text-white round
{{ strtoupper(substr($scheduled->user->name, 0, 1)) }}
- {{ $scheduled->userDish?->dish?->name ?? 'Skipped' }} + {{ $scheduled->userDish?->dish?->name ?? ($scheduled->skip_reason ?: 'Skipped') }} @@ -143,7 +143,7 @@ class="w-7 h-7 bg-gray-600 hover:bg-primary text-gray-300 hover:text-white round {{ strtoupper(substr($scheduled->user->name, 0, 1)) }}
-
{{ $scheduled->userDish?->dish?->name ?? 'Skipped' }}
+
{{ $scheduled->userDish?->dish?->name ?? ($scheduled->skip_reason ?: 'Skipped') }}
{{ $scheduled->user->name }}
@@ -212,6 +212,36 @@ class="px-4 py-2 bg-warning text-white rounded hover:bg-yellow-600 transition-co @endif + + @if($showSkipModal) +
+
+

Skip Day

+

+ Skip this meal? Optionally add a reason, e.g. "eating out". +

+ +
+ + +
+ +
+ + +
+
+
+ @endif + @if($showEditDishModal)
diff --git a/src/DishPlanner/ScheduledUserDish/Actions/SkipScheduledUserDishForDateAction.php b/src/DishPlanner/ScheduledUserDish/Actions/SkipScheduledUserDishForDateAction.php index 441093c..6ab7e7b 100644 --- a/src/DishPlanner/ScheduledUserDish/Actions/SkipScheduledUserDishForDateAction.php +++ b/src/DishPlanner/ScheduledUserDish/Actions/SkipScheduledUserDishForDateAction.php @@ -9,7 +9,7 @@ class SkipScheduledUserDishForDateAction { - public function execute(Planner $planner, Carbon $date, int $userId): bool + public function execute(Planner $planner, Carbon $date, int $userId, ?string $reason = null): bool { $schedule = Schedule::query() ->where('planner_id', $planner->id) @@ -32,6 +32,7 @@ public function execute(Planner $planner, Carbon $date, int $userId): bool $scheduledUserDish->update([ 'is_skipped' => true, 'user_dish_id' => null, + 'skip_reason' => $reason, ]); return true; diff --git a/tests/Feature/Schedule/ScheduleCalendarTest.php b/tests/Feature/Schedule/ScheduleCalendarTest.php new file mode 100644 index 0000000..6fcb2dd --- /dev/null +++ b/tests/Feature/Schedule/ScheduleCalendarTest.php @@ -0,0 +1,107 @@ +create(); + $this->planner = $planner; + } + + public function test_skip_day_opens_modal_and_persists_reason(): void + { + $planner = $this->planner; + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $dish->users()->attach($user); + + $date = now(); + $schedule = Schedule::create([ + 'planner_id' => $planner->id, + 'date' => $date->format('Y-m-d'), + 'is_skipped' => false, + ]); + + $userDish = UserDish::query()->where('user_id', $user->id)->firstOrFail(); + ScheduledUserDish::create([ + 'schedule_id' => $schedule->id, + 'user_id' => $user->id, + 'user_dish_id' => $userDish->id, + 'is_skipped' => false, + ]); + + $this->actingAs($planner); + + Livewire::test(ScheduleCalendar::class) + ->call('skipDay', $date->format('Y-m-d'), $user->id) + ->assertSet('showSkipModal', true) + ->assertSet('skipDate', $date->format('Y-m-d')) + ->set('skipReason', 'eating out') + ->call('confirmSkip') + ->assertSee('eating out'); + + $this->assertDatabaseHas(ScheduledUserDish::class, [ + 'schedule_id' => $schedule->id, + 'user_id' => $user->id, + 'is_skipped' => true, + 'user_dish_id' => null, + 'skip_reason' => 'eating out', + ]); + } + + public function test_skip_day_without_reason_persists_null(): void + { + $planner = $this->planner; + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $dish->users()->attach($user); + + $date = now(); + $schedule = Schedule::create([ + 'planner_id' => $planner->id, + 'date' => $date->format('Y-m-d'), + 'is_skipped' => false, + ]); + + $userDish = UserDish::query()->where('user_id', $user->id)->firstOrFail(); + ScheduledUserDish::create([ + 'schedule_id' => $schedule->id, + 'user_id' => $user->id, + 'user_dish_id' => $userDish->id, + 'is_skipped' => false, + ]); + + $this->actingAs($planner); + + Livewire::test(ScheduleCalendar::class) + ->call('skipDay', $date->format('Y-m-d'), $user->id) + ->call('confirmSkip'); + + $this->assertDatabaseHas(ScheduledUserDish::class, [ + 'schedule_id' => $schedule->id, + 'user_id' => $user->id, + 'is_skipped' => true, + 'skip_reason' => null, + ]); + } +} diff --git a/tests/Unit/ScheduledUserDish/Actions/SkipScheduledUserDishForDateActionTest.php b/tests/Unit/ScheduledUserDish/Actions/SkipScheduledUserDishForDateActionTest.php index ab45dc1..0b30028 100644 --- a/tests/Unit/ScheduledUserDish/Actions/SkipScheduledUserDishForDateActionTest.php +++ b/tests/Unit/ScheduledUserDish/Actions/SkipScheduledUserDishForDateActionTest.php @@ -6,6 +6,7 @@ use App\Models\Schedule; use App\Models\ScheduledUserDish; use App\Models\User; +use App\Models\UserDish; use Carbon\Carbon; use DishPlanner\ScheduledUserDish\Actions\SkipScheduledUserDishForDateAction; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -59,6 +60,40 @@ public function test_skips_scheduled_user_dish(): void ]); } + public function test_skips_with_reason_stores_reason(): void + { + $planner = $this->planner; + $user = User::factory()->planner($planner)->create(); + $dish = Dish::factory()->planner($planner)->create(); + $dish->users()->attach($user); + + $date = Carbon::parse('2026-01-15'); + $schedule = Schedule::create([ + 'planner_id' => $planner->id, + 'date' => $date->format('Y-m-d'), + 'is_skipped' => false, + ]); + + $userDish = UserDish::query()->where('user_id', $user->id)->firstOrFail(); + ScheduledUserDish::create([ + 'schedule_id' => $schedule->id, + 'user_id' => $user->id, + 'user_dish_id' => $userDish->id, + 'is_skipped' => false, + ]); + + $result = $this->action->execute($planner, $date, $user->id, 'eating out'); + + $this->assertTrue($result); + $this->assertDatabaseHas(ScheduledUserDish::class, [ + 'schedule_id' => $schedule->id, + 'user_id' => $user->id, + 'is_skipped' => true, + 'user_dish_id' => null, + 'skip_reason' => 'eating out', + ]); + } + public function test_returns_false_when_schedule_does_not_exist(): void { $planner = $this->planner;