From d26ba5e5e67ff8f6469ddf0e656f322897656090 Mon Sep 17 00:00:00 2001 From: E98Developer Date: Mon, 24 Aug 2026 15:59:59 +0200 Subject: [PATCH] =?UTF-8?q?FIX=20EV3-466=20h=C3=A9tv=C3=A9gi=20sz=C3=A1ll?= =?UTF-8?q?=C3=ADt=C3=A1si=20napok=20enged=C3=A9lyez=C3=A9se=20a=20heti=20?= =?UTF-8?q?sablonb=C3=B3l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A DeliveryCalendarService 4. prioritása a WorkCalendarService::isWorkDay()-t hívta, ami a naptárban nem szereplő napoknál a hétvégére esett vissza. Így egy hétvégét is tartalmazó sablon szombat/vasárnap mezőit sosem érte el a kiértékelés (5. prioritás), és csak egyedi felülbírálással lehetett hétvégi szállítást engedni. - WorkCalendarService: új isHoliday(), ami csak a work_calendars ünnepnap rekordjaira ad true-t, a naptári hétvégére nem. Az isWorkDay() változatlan. - DeliveryCalendarService: a 4. prioritás mostantól isHoliday()-t használ, így a hétvégéről a heti sablon dönt. Ünnepnap továbbra is tilt. - NextDeliveryDateCalculatorService: a szállítási nap keresése kikerült az isWorkDay() blokkból, különben a hétvége a következő szállítási nap számításánál továbbra is kimaradt volna. Az átfutási időbe az új countsTowardLeadTime() szerint a sablon hétvégi napjai is beleszámítanak, a H-P sablonok viselkedése változatlan. - DeliveryCalendarWidget: a szürke "inaktív" festés csak a nem szállítási hétvégékre megy rá. Mellékesen a szállítási napok ciklusa $date->addDay()-jel mutálta a kollekció Carbon objektumait, ez copy()-ra javítva. 7 új teszt, köztük a H-P sablon regresszió-őre. Co-Authored-By: Claude Opus 5 --- .../Widgets/DeliveryCalendarWidget.php | 11 ++- app/Services/DeliveryCalendarService.php | 8 +- .../NextDeliveryDateCalculatorService.php | 51 +++++++++--- app/Services/WorkCalendarService.php | 15 ++++ tests/Feature/DeliveryCalendarServiceTest.php | 75 +++++++++++++++++ .../NextDeliveryDateCalculatorServiceTest.php | 81 +++++++++++++++++++ 6 files changed, 225 insertions(+), 16 deletions(-) create mode 100644 tests/Feature/NextDeliveryDateCalculatorServiceTest.php diff --git a/app/Filament/Widgets/DeliveryCalendarWidget.php b/app/Filament/Widgets/DeliveryCalendarWidget.php index c170cd9..d9755e3 100644 --- a/app/Filament/Widgets/DeliveryCalendarWidget.php +++ b/app/Filament/Widgets/DeliveryCalendarWidget.php @@ -139,12 +139,17 @@ public function fetchEvents(array $info): array $assignment ); + // A szállítási napok halmaza a hétvége-festéshez (4. pont) is kell. + $deliveryDayLookup = array_flip( + $deliveryDates->map(fn (Carbon $date) => $date->format('Y-m-d'))->all() + ); + foreach ($deliveryDates as $date) { $events[] = [ 'id' => 'delivery-' . $date->format('Y-m-d'), 'title' => '', 'start' => $date->format('Y-m-d'), - 'end' => $date->addDay()->format('Y-m-d'), + 'end' => $date->copy()->addDay()->format('Y-m-d'), 'allDay' => true, 'display' => 'background', 'backgroundColor' => '#4CAF50', @@ -182,9 +187,11 @@ public function fetchEvents(array $info): array } // 4. Hétvégék – szürke háttér (inaktív napok) + // Csak azok a hétvégi napok, amelyek nem szállítási napok: a heti sablon + // tartalmazhatja a szombatot/vasárnapot is (EV3-466). $current = $start->copy(); while ($current->lte($end)) { - if ($current->isWeekend()) { + if ($current->isWeekend() && ! isset($deliveryDayLookup[$current->format('Y-m-d')])) { $events[] = [ 'id' => 'weekend-' . $current->format('Y-m-d'), 'title' => '', diff --git a/app/Services/DeliveryCalendarService.php b/app/Services/DeliveryCalendarService.php index d384473..dd294ed 100644 --- a/app/Services/DeliveryCalendarService.php +++ b/app/Services/DeliveryCalendarService.php @@ -72,9 +72,11 @@ public function isDeliveryDay(ProfitCenter $pc, Supplier $supplier, Carbon $date return $override->is_delivery_day; } - // 4. Prioritás: Hivatalos ünnepnap (WorkCalendar) - // Ha aznap nem hivatalos munkanap van, akkor nincs szállítás (kivéve ha az 1-3. pont felülbírálta). - if (! $this->workCalendarService->isWorkDay($date)) { + // 4. Prioritás: Hivatalos munkaszüneti nap (WorkCalendar) + // Csak a naptárban ünnepnapként rögzített dátum tilt. A naptári hétvége önmagában + // NEM: azt az 5. pont heti sablonja dönti el, különben egy hétvégét is tartalmazó + // sablon napjai elérhetetlenek maradnának (EV3-466). + if ($this->workCalendarService->isHoliday($date)) { return false; } diff --git a/app/Services/NextDeliveryDateCalculatorService.php b/app/Services/NextDeliveryDateCalculatorService.php index 93c7aca..47e7e67 100644 --- a/app/Services/NextDeliveryDateCalculatorService.php +++ b/app/Services/NextDeliveryDateCalculatorService.php @@ -62,36 +62,65 @@ public function calculate( } } - return $this->findNextDeliveryDay($startDay, $supplier, $pc, $leadDays); + return $this->findNextDeliveryDay($startDay, $supplier, $pc, $leadDays, $assignment); } /** - * Munkanapokban számol előre ($leadDays darabot), majd az első olyan napot adja vissza, + * Előreszámol $leadDays darab átfutási napot, majd az első olyan napot adja vissza, * amelyik szállítási nap is. + * + * A szállítási nap keresése NINCS munkanaphoz kötve: hétvégi szállítást a heti sablon + * engedhet, ezt az isDeliveryDay() dönti el (EV3-466). */ private function findNextDeliveryDay( Carbon $candidate, Supplier $supplier, ProfitCenter $pc, int $leadDays, + ProfitCenterSupplierSchedule $assignment, ): ?Carbon { - $workingDaysCounted = 0; + $leadDaysCounted = 0; // Max. 365 nap iteráció (végtelen ciklus-védelem) for ($i = 0; $i < 365; $i++) { - if ($this->workCalendarService->isWorkDay($candidate)) { - if ($workingDaysCounted >= $leadDays) { - // Átfutási idő letelt – az első szállítási napot keressük - if ($this->deliveryCalendarService->isDeliveryDay($pc, $supplier, $candidate)) { - return $candidate->copy(); - } - } else { - $workingDaysCounted++; + if ($leadDaysCounted >= $leadDays) { + // Átfutási idő letelt – az első szállítási napot keressük + if ($this->deliveryCalendarService->isDeliveryDay($pc, $supplier, $candidate, $assignment)) { + return $candidate->copy(); } + } elseif ($this->countsTowardLeadTime($candidate, $assignment)) { + $leadDaysCounted++; } $candidate->addDay(); } return null; } + + /** + * Eldönti, hogy az adott nap beleszámít-e az átfutási idő visszaszámolásába. + * + * A hivatalos munkanapokon túl beleszámítanak azok a hétvégi napok is, amelyeket a + * PC-hez rendelt heti sablon szállítási napként jelöl – egy 7 napos sablonnál tehát + * minden nap telik, egy H–P sablonnál viszont a hétvége változatlanul nem (EV3-466). + * Munkaszüneti napon sosem telik az átfutás. + */ + private function countsTowardLeadTime(Carbon $date, ProfitCenterSupplierSchedule $assignment): bool + { + if ($this->workCalendarService->isHoliday($date)) { + return false; + } + + if ($this->workCalendarService->isWorkDay($date)) { + return true; + } + + $schedule = $assignment->deliverySchedule; + + if (! $schedule || ! $schedule->is_active) { + return false; + } + + return (bool) $schedule->{strtolower($date->format('l'))}; + } } diff --git a/app/Services/WorkCalendarService.php b/app/Services/WorkCalendarService.php index 1abc7e9..04327cb 100644 --- a/app/Services/WorkCalendarService.php +++ b/app/Services/WorkCalendarService.php @@ -110,6 +110,21 @@ public function isWorkDay(Carbon $date): bool return !$date->isWeekend(); } + /** + * Megállapítja, hogy az adott nap hivatalos munkaszüneti nap-e. + * + * Az isWorkDay()-jel ellentétben a puszta naptári hétvégét NEM tekinti annak: + * csak a work_calendars táblában ünnepnapként rögzített dátumra ad true-t. + * A szállítási naptár ezt használja, hogy a heti sablon engedhessen hétvégi + * szállítást is (EV3-466). + */ + public function isHoliday(Carbon $date): bool + { + $record = WorkCalendar::where('date', $date->toDateString())->first(); + + return $record?->type === WorkDayType::Holiday; + } + /** * Kiszámítja a következő érvényes munkanapot. */ diff --git a/tests/Feature/DeliveryCalendarServiceTest.php b/tests/Feature/DeliveryCalendarServiceTest.php index 395804a..33ab03b 100644 --- a/tests/Feature/DeliveryCalendarServiceTest.php +++ b/tests/Feature/DeliveryCalendarServiceTest.php @@ -264,3 +264,78 @@ expect($dateStrings)->toContain('2026-04-15'); expect($dateStrings)->toContain('2026-04-17'); }); + +it('allows weekend delivery when the schedule includes it', function () { + $pc = ProfitCenter::factory()->create(); + $supplier = Supplier::factory()->create(); + $schedule = DeliverySchedule::factory()->create(['saturday' => true]); + ProfitCenterSupplierSchedule::factory()->create([ + 'profit_center_id' => $pc->id, + 'supplier_id' => $supplier->id, + 'delivery_schedule_id' => $schedule->id, + ]); + $service = app(DeliveryCalendarService::class); + $saturday = Carbon::parse('2026-04-18'); // Saturday + expect($service->isDeliveryDay($pc, $supplier, $saturday))->toBeTrue(); +}); + +it('still blocks weekends the schedule does not include', function () { + $pc = ProfitCenter::factory()->create(); + $supplier = Supplier::factory()->create(); + $schedule = DeliverySchedule::factory()->create(['monday' => true, 'saturday' => false]); + ProfitCenterSupplierSchedule::factory()->create([ + 'profit_center_id' => $pc->id, + 'supplier_id' => $supplier->id, + 'delivery_schedule_id' => $schedule->id, + ]); + $service = app(DeliveryCalendarService::class); + $saturday = Carbon::parse('2026-04-18'); // Saturday + expect($service->isDeliveryDay($pc, $supplier, $saturday))->toBeFalse(); +}); + +it('blocks official holidays even on a scheduled weekend day', function () { + $pc = ProfitCenter::factory()->create(); + $supplier = Supplier::factory()->create(); + $schedule = DeliverySchedule::factory()->create(['saturday' => true]); + ProfitCenterSupplierSchedule::factory()->create([ + 'profit_center_id' => $pc->id, + 'supplier_id' => $supplier->id, + 'delivery_schedule_id' => $schedule->id, + ]); + WorkCalendar::create([ + 'date' => '2026-04-18', + 'type' => WorkDayType::Holiday, + 'data_source' => DataSource::Manual, + ]); + $service = app(DeliveryCalendarService::class); + $saturday = Carbon::parse('2026-04-18'); // Saturday + expect($service->isDeliveryDay($pc, $supplier, $saturday))->toBeFalse(); +}); + +it('includes scheduled weekend days in an available date range', function () { + $pc = ProfitCenter::factory()->create(); + $supplier = Supplier::factory()->create(); + $schedule = DeliverySchedule::factory()->create([ + 'monday' => true, + 'tuesday' => true, + 'wednesday' => true, + 'thursday' => true, + 'friday' => true, + 'saturday' => true, + 'sunday' => true, + ]); + ProfitCenterSupplierSchedule::factory()->create([ + 'profit_center_id' => $pc->id, + 'supplier_id' => $supplier->id, + 'delivery_schedule_id' => $schedule->id, + ]); + $service = app(DeliveryCalendarService::class); + $dates = $service->getAvailableDeliveryDates( + $pc, + $supplier, + Carbon::parse('2026-04-13'), // Monday + Carbon::parse('2026-04-19'), // Sunday + ); + + expect($dates)->toHaveCount(7); +}); diff --git a/tests/Feature/NextDeliveryDateCalculatorServiceTest.php b/tests/Feature/NextDeliveryDateCalculatorServiceTest.php new file mode 100644 index 0000000..fbd6d11 --- /dev/null +++ b/tests/Feature/NextDeliveryDateCalculatorServiceTest.php @@ -0,0 +1,81 @@ + $days + */ +function makeAssignment(array $days, bool $hasConstraint = true): array +{ + $pc = ProfitCenter::factory()->create(); + $supplier = Supplier::factory()->create([ + 'hasDeliveryConstraint' => $hasConstraint, + 'orderCutOffTime' => 12, + 'deliveryLeadTime' => 48, // 2 nap + ]); + $schedule = DeliverySchedule::factory()->create($days); + ProfitCenterSupplierSchedule::factory()->create([ + 'profit_center_id' => $pc->id, + 'supplier_id' => $supplier->id, + 'delivery_schedule_id' => $schedule->id, + ]); + + return [$pc, $supplier]; +} + +const EVERY_DAY = [ + 'monday' => true, + 'tuesday' => true, + 'wednesday' => true, + 'thursday' => true, + 'friday' => true, + 'saturday' => true, + 'sunday' => true, +]; + +const WEEKDAYS_ONLY = [ + 'monday' => true, + 'tuesday' => true, + 'wednesday' => true, + 'thursday' => true, + 'friday' => true, +]; + +it('counts scheduled weekend days towards the lead time', function () { + [$pc, $supplier] = makeAssignment(EVERY_DAY); + + $result = app(NextDeliveryDateCalculatorService::class) + ->calculate($supplier->id, $pc->id, Carbon::parse('2026-04-17 08:00')); // Friday, cut-off előtt + + // Péntek (1.) + szombat (2.) telik el az átfutásból, így vasárnap már szállítható. + expect($result?->toDateString())->toBe('2026-04-19'); // Sunday +}); + +it('does not count weekends for a weekday-only schedule', function () { + [$pc, $supplier] = makeAssignment(WEEKDAYS_ONLY); + + $result = app(NextDeliveryDateCalculatorService::class) + ->calculate($supplier->id, $pc->id, Carbon::parse('2026-04-17 08:00')); // Friday, cut-off előtt + + // Péntek (1.) + hétfő (2.) telik el – a hétvége kimarad –, így kedd az első szállítási nap. + expect($result?->toDateString())->toBe('2026-04-21'); // Tuesday +}); + +it('returns a weekend day when there is no delivery constraint', function () { + [$pc, $supplier] = makeAssignment(EVERY_DAY, hasConstraint: false); + + $result = app(NextDeliveryDateCalculatorService::class) + ->calculate($supplier->id, $pc->id, Carbon::parse('2026-04-18 08:00')); // Saturday + + // Átfutási idő nélkül a szombat maga az első szállítási nap. + expect($result?->toDateString())->toBe('2026-04-18'); // Saturday +});