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;