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;
|
public $regenerateUserId = null;
|
||||||
|
|
||||||
|
// Skip modal
|
||||||
|
public bool $showSkipModal = false;
|
||||||
|
|
||||||
|
public ?string $skipDate = null;
|
||||||
|
|
||||||
|
public ?int $skipUserId = null;
|
||||||
|
|
||||||
|
public string $skipReason = '';
|
||||||
|
|
||||||
// Edit dish modal
|
// Edit dish modal
|
||||||
public $showEditDishModal = false;
|
public $showEditDishModal = false;
|
||||||
|
|
||||||
|
|
@ -145,26 +154,52 @@ public function confirmRegenerate(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
public function skipDay($date, $userId): 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 {
|
try {
|
||||||
if (! $this->authorizeUser($userId)) {
|
if (! $this->authorizeUser((int) $this->skipUserId)) {
|
||||||
session()->flash('error', 'Unauthorized action.');
|
session()->flash('error', 'Unauthorized action.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->validate([
|
||||||
|
'skipReason' => ['nullable', 'string', 'max:255'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$reason = trim($this->skipReason) ?: null;
|
||||||
|
|
||||||
$action = new SkipScheduledUserDishForDateAction;
|
$action = new SkipScheduledUserDishForDateAction;
|
||||||
$action->execute(
|
$action->execute(
|
||||||
auth()->user(),
|
auth()->user(),
|
||||||
Carbon::parse($date),
|
Carbon::parse($this->skipDate),
|
||||||
$userId
|
$this->skipUserId,
|
||||||
|
$reason
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->showSkipModal = false;
|
||||||
|
$this->skipDate = null;
|
||||||
|
$this->skipUserId = null;
|
||||||
|
$this->skipReason = '';
|
||||||
|
|
||||||
$this->loadCalendar();
|
$this->loadCalendar();
|
||||||
|
|
||||||
session()->flash('success', 'Day skipped successfully!');
|
session()->flash('success', 'Day skipped successfully!');
|
||||||
} catch (Exception $e) {
|
} 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.');
|
session()->flash('error', 'Unable to skip day. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -181,6 +216,10 @@ public function cancel(): void
|
||||||
$this->showRegenerateModal = false;
|
$this->showRegenerateModal = false;
|
||||||
$this->regenerateDate = null;
|
$this->regenerateDate = null;
|
||||||
$this->regenerateUserId = null;
|
$this->regenerateUserId = null;
|
||||||
|
$this->showSkipModal = false;
|
||||||
|
$this->skipDate = null;
|
||||||
|
$this->skipUserId = null;
|
||||||
|
$this->skipReason = '';
|
||||||
$this->showEditDishModal = false;
|
$this->showEditDishModal = false;
|
||||||
$this->editDate = null;
|
$this->editDate = null;
|
||||||
$this->editUserId = null;
|
$this->editUserId = null;
|
||||||
|
|
@ -436,6 +475,7 @@ public function saveDish(): void
|
||||||
[
|
[
|
||||||
'user_dish_id' => $userDish->id,
|
'user_dish_id' => $userDish->id,
|
||||||
'is_skipped' => false,
|
'is_skipped' => false,
|
||||||
|
'skip_reason' => null,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
* @property int $user_dish_id
|
* @property int $user_dish_id
|
||||||
* @property UserDish $userDish
|
* @property UserDish $userDish
|
||||||
* @property bool $is_skipped
|
* @property bool $is_skipped
|
||||||
|
* @property string|null $skip_reason
|
||||||
*
|
*
|
||||||
* @method static create(array $array)
|
* @method static create(array $array)
|
||||||
* @method static ScheduledUserDishFactory factory($count = null, $state = [])
|
* @method static ScheduledUserDishFactory factory($count = null, $state = [])
|
||||||
|
|
@ -31,6 +32,7 @@ class ScheduledUserDish extends Model
|
||||||
'user_id',
|
'user_id',
|
||||||
'user_dish_id',
|
'user_dish_id',
|
||||||
'is_skipped',
|
'is_skipped',
|
||||||
|
'skip_reason',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
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">
|
<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)) }}
|
{{ strtoupper(substr($scheduled->user->name, 0, 1)) }}
|
||||||
</div>
|
</div>
|
||||||
<span class="truncate">{{ $scheduled->userDish?->dish?->name ?? 'Skipped' }}</span>
|
<span class="truncate">{{ $scheduled->userDish?->dish?->name ?? ($scheduled->skip_reason ?: 'Skipped') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- 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)) }}
|
{{ strtoupper(substr($scheduled->user->name, 0, 1)) }}
|
||||||
</div>
|
</div>
|
||||||
<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 class="text-xs opacity-75">{{ $scheduled->user->name }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -212,6 +212,36 @@ class="px-4 py-2 bg-warning text-white rounded hover:bg-yellow-600 transition-co
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@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 -->
|
<!-- Edit Dish Modal -->
|
||||||
@if($showEditDishModal)
|
@if($showEditDishModal)
|
||||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
class SkipScheduledUserDishForDateAction
|
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()
|
$schedule = Schedule::query()
|
||||||
->where('planner_id', $planner->id)
|
->where('planner_id', $planner->id)
|
||||||
|
|
@ -32,6 +32,7 @@ public function execute(Planner $planner, Carbon $date, int $userId): bool
|
||||||
$scheduledUserDish->update([
|
$scheduledUserDish->update([
|
||||||
'is_skipped' => true,
|
'is_skipped' => true,
|
||||||
'user_dish_id' => null,
|
'user_dish_id' => null,
|
||||||
|
'skip_reason' => $reason,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return true;
|
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\Schedule;
|
||||||
use App\Models\ScheduledUserDish;
|
use App\Models\ScheduledUserDish;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\UserDish;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DishPlanner\ScheduledUserDish\Actions\SkipScheduledUserDishForDateAction;
|
use DishPlanner\ScheduledUserDish\Actions\SkipScheduledUserDishForDateAction;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
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
|
public function test_returns_false_when_schedule_does_not_exist(): void
|
||||||
{
|
{
|
||||||
$planner = $this->planner;
|
$planner = $this->planner;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue