51 - Indicate eating out when skipping a meal
This commit is contained in:
parent
d8c1830c71
commit
01659923fa
7 changed files with 244 additions and 7 deletions
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
107
tests/Feature/Schedule/ScheduleCalendarTest.php
Normal file
107
tests/Feature/Schedule/ScheduleCalendarTest.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue