51 - Indicate eating out when skipping a meal

This commit is contained in:
myrmidex 2026-08-18 22:01:59 +02:00
parent d8c1830c71
commit 01659923fa
7 changed files with 244 additions and 7 deletions

View file

@ -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;
@ -146,25 +155,51 @@ public function confirmRegenerate(): void
public function skipDay($date, $userId): void
{
try {
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((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,
]
);

View file

@ -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 = [

View file

@ -0,0 +1,22 @@
<?php
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('scheduled_user_dishes', function (Blueprint $table) {
$table->string('skip_reason')->nullable()->after('is_skipped');
});
}
public function down(): void
{
Schema::table('scheduled_user_dishes', function (Blueprint $table) {
$table->dropColumn('skip_reason');
});
}
};

View file

@ -68,7 +68,7 @@ class="w-5 h-5 bg-gray-600 hover:bg-primary text-gray-300 hover:text-white round
<div class="w-4 h-4 bg-white text-primary rounded-full flex items-center justify-center text-xs font-bold mr-1">
{{ strtoupper(substr($scheduled->user->name, 0, 1)) }}
</div>
<span class="truncate">{{ $scheduled->userDish?->dish?->name ?? 'Skipped' }}</span>
<span class="truncate">{{ $scheduled->userDish?->dish?->name ?? ($scheduled->skip_reason ?: 'Skipped') }}</span>
</div>
<!-- Action buttons -->
@ -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)) }}
</div>
<div>
<div class="font-medium">{{ $scheduled->userDish?->dish?->name ?? 'Skipped' }}</div>
<div class="font-medium">{{ $scheduled->userDish?->dish?->name ?? ($scheduled->skip_reason ?: 'Skipped') }}</div>
<div class="text-xs opacity-75">{{ $scheduled->user->name }}</div>
</div>
</div>
@ -212,6 +212,36 @@ class="px-4 py-2 bg-warning text-white rounded hover:bg-yellow-600 transition-co
</div>
@endif
<!-- Skip Modal -->
@if($showSkipModal)
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-gray-600 border-2 border-secondary rounded-lg p-6 w-full max-w-md mx-4">
<h2 class="text-xl text-accent-blue mb-4">Skip Day</h2>
<p class="text-gray-100 mb-6">
Skip this meal? Optionally add a reason, e.g. "eating out".
</p>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Reason (optional)</label>
<input type="text" wire:model="skipReason" maxlength="255"
placeholder="e.g. eating out"
class="w-full p-2 border rounded bg-gray-700 border-secondary text-gray-100 focus:bg-gray-900 focus:outline-none focus:border-accent-blue">
</div>
<div class="flex justify-end space-x-3">
<button wire:click="cancel"
class="px-4 py-2 border-2 border-secondary text-gray-100 rounded hover:bg-gray-700 transition-colors duration-200">
Cancel
</button>
<button wire:click="confirmSkip"
class="px-4 py-2 bg-warning text-white rounded hover:bg-yellow-600 transition-colors duration-200">
Skip
</button>
</div>
</div>
</div>
@endif
<!-- Edit Dish Modal -->
@if($showEditDishModal)
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">

View file

@ -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;

View file

@ -0,0 +1,107 @@
<?php
namespace Tests\Feature\Schedule;
use App\Livewire\Schedule\ScheduleCalendar;
use App\Models\Dish;
use App\Models\Planner;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use App\Models\UserDish;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
class ScheduleCalendarTest extends TestCase
{
use RefreshDatabase;
protected Planner $planner;
protected function setUp(): void
{
parent::setUp();
/** @var Planner $planner */
$planner = Planner::factory()->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,
]);
}
}

View file

@ -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;