EV3-439 Szállítási korlátozások

This commit is contained in:
E98Developer 2026-06-07 21:25:03 +02:00
parent cf0e1dfee9
commit 57c654db2d
7 changed files with 230 additions and 84 deletions

View File

@ -42,8 +42,11 @@ class OrderController extends Controller
protected bool $testMode = false; protected bool $testMode = false;
public function __construct(OrderService $service) public function __construct(
{ OrderService $service,
protected DeliveryCalendarService $deliveryCalendarService,
protected NextDeliveryDateCalculatorService $nextDeliveryDateCalculator,
) {
$this->service = $service; $this->service = $service;
/* /*
need debugger need debugger
@ -764,7 +767,7 @@ private function getParsedOrderDataFromSession()
$orderData['sumPrice'] = 0; $orderData['sumPrice'] = 0;
foreach ($products as $key => $product) { foreach ($products as $key => $product) {
$quantity = $orderData['productSelected'][$product['id']]; $quantity = $orderData['productSelected'][$product['id']];
if ($quantity > 0) { if ($quantity > 0 && ! empty($product['last_price'][0]['pivot']['price'])) {
$price = $product['last_price'][0]['pivot']['price']; $price = $product['last_price'][0]['pivot']['price'];
$products[$key]['quantity'] = $quantity; $products[$key]['quantity'] = $quantity;
@ -1640,16 +1643,33 @@ public function getEmailAttachment($orderData, $fileType): array
private function updateSend(Request $request, int $orderId): JsonResponse private function updateSend(Request $request, int $orderId): JsonResponse
{ {
if (! $Order = Order::find($orderId)) { if (! $Order = Order::with('supplier', 'profitCenter')->find($orderId)) {
return \response()->json(['success' => false], 500); return \response()->json(['success' => false], 500);
} }
$diffDay = Carbon::parse($Order->deliveryDate)->diffInDays(Carbon::now(), false);
if ($diffDay > 0) { $supplier = $Order->supplier;
return \response()->json([ $pc = $Order->profitCenter;
'success' => false, $deliveryDate = ICarbon::parse($Order->deliveryDate)->startOfDay();
'errors' => ['Nem megfelelő dátum ['.$Order->deliveryDate.']'],
], 500); // Szállítói korlátozás esetén a tényleges küldés pillanatában is ellenőrizzük,
// hogy a kiválasztott szállítási dátum még mindig megfelel-e az üzleti szabályoknak.
// (Piszkozat újranyitása vagy időközbeni override miatt időközben érvénytelenné válhatott.)
// Kettős ellenőrzés: isDeliveryDay (sablon/override) + calculate (cut-off, lead time).
if ($supplier && $pc && $supplier->hasDeliveryConstraint) {
$nextAvailable = $this->nextDeliveryDateCalculator
->calculate($supplier->id, $pc->id, ICarbon::now());
$isInvalid = ! $this->deliveryCalendarService->isDeliveryDay($pc, $supplier, $deliveryDate)
|| ($nextAvailable && $deliveryDate->lt($nextAvailable->copy()->startOfDay()));
if ($isInvalid) {
return \response()->json([
'success' => false,
'errorCode' => 'INVALID_DELIVERY_DATE',
'errors' => ['A kiválasztott szállítási dátum már nem megfelelő ['.$Order->deliveryDate.']'],
'suggestedDeliveryDate' => $nextAvailable?->format('Y-m-d'),
], 422);
}
} }
/* /*
@ -1936,18 +1956,78 @@ public function getDataTableContent(): JsonResponse
public function checkReadyToSend(Request $request, int $orderId): JsonResponse public function checkReadyToSend(Request $request, int $orderId): JsonResponse
{ {
$readyToSend = true; $readyToSend = true;
$orderData = $this->getParsedOrderDataFromSession();
debug($orderData);
$why = []; $why = [];
if (! isset($orderData['supplierId']) || $orderData['supplierId'] < 1) { $suggestedDeliveryDate = null;
// A DB-ből töltjük be az aktuális rendelési adatokat a session nem megbízható,
// mert piszkozatnál eltérhet a mentett állapottól.
$Order = Order::with('supplier', 'profitCenter')->find($orderId);
if (! $Order) {
return \response()->json(['success' => false, 'readyToSend' => false, 'why' => ['notFound'], 'suggestedDeliveryDate' => null]);
}
// supplierId ellenőrzés a DB-ből
if (! $Order->supplier_id || $Order->supplier_id < 1) {
$why[] = 'supplierId'; $why[] = 'supplierId';
$readyToSend = false; $readyToSend = false;
} }
if (! isset($orderData['items']) || ! is_array($orderData['items']) || count($orderData['items']) < 1) {
// productSelected (items) ellenőrzés a DB-ből
$productSelected = is_array($Order->productSelected)
? $Order->productSelected
: json_decode($Order->productSelected, true);
$hasItems = is_array($productSelected) && count(array_filter($productSelected, fn ($q) => (int) $q > 0)) > 0;
if (! $hasItems) {
$why[] = 'items'; $why[] = 'items';
$readyToSend = false; $readyToSend = false;
} }
return \response()->json(['success' => true, 'readyToSend' => $readyToSend, 'orderData' => $orderData, 'why' => $why]); // UX-réteg: a send nézet betöltésekor jelezzük, ha a kiválasztott szállítási dátum
// időközben (pl. piszkozatból visszatöltve) érvénytelenné vált a szállítói korlátozások miatt.
// Az ellenőrzés két feltételt vizsgál:
// 1. isDeliveryDay: az adott nap egyáltalán szállítási nap-e (sablon, override, ünnepnap)
// 2. A dátum nem korábbi-e a legközelebbi rendelhető napnál (cut-off és lead time figyelembevételével)
if (! empty($Order->deliveryDate)) {
$supplier = $Order->supplier;
$pc = $Order->profitCenter;
$deliveryDate = ICarbon::parse($Order->deliveryDate)->startOfDay();
if ($supplier && $pc && $supplier->hasDeliveryConstraint) {
$nextAvailable = $this->nextDeliveryDateCalculator
->calculate($supplier->id, $pc->id, ICarbon::now());
$isDeliveryDay = $this->deliveryCalendarService->isDeliveryDay($pc, $supplier, $deliveryDate->copy());
$isEarlierThanNext = $nextAvailable && $deliveryDate->copy()->startOfDay()->lt($nextAvailable->copy()->startOfDay());
$isInvalid = ! $isDeliveryDay || $isEarlierThanNext;
\Illuminate\Support\Facades\Log::info('checkReadyToSend deliveryDate check', [
'orderId' => $orderId,
'deliveryDate' => $deliveryDate->toDateString(),
'nextAvailable' => $nextAvailable?->toDateString(),
'isDeliveryDay' => $isDeliveryDay,
'isEarlierThanNext' => $isEarlierThanNext,
'isInvalid' => $isInvalid,
]);
if ($isInvalid) {
$why[] = 'deliveryDate';
$readyToSend = false;
$suggestedDeliveryDate = $nextAvailable?->format('Y-m-d');
}
}
}
// Az orderData-t a sessionből adjuk vissza a frontend kompatibilitás érdekében,
// de a validációk már a DB adatain alapulnak.
$orderData = $this->getParsedOrderDataFromSession();
return \response()->json([
'success' => true,
'readyToSend' => $readyToSend,
'orderData' => $orderData,
'why' => $why,
'suggestedDeliveryDate' => $suggestedDeliveryDate,
]);
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,9 @@
{ {
"/js/app.js": "/js/app.js?id=788435874adbbf8e5ddc27cc9d18e00e", "/js/app.js": "/js/app.js?id=788435874adbbf8e5ddc27cc9d18e00e",
"/js/admin.js": "/js/admin.js?id=63aa33642e8f8f48a97c8a7bb91ce5d8", "/js/admin.js": "/js/admin.js?id=e191eeeb2d53e4dae1bffd489d0cd7e1",
"/js/module.js": "/js/module.js?id=29a456e6546fb80088a21e164cdfca82", "/js/module.js": "/js/module.js?id=e182fe1a0a0338350194e2304def7185",
"/js/components.js": "/js/components.js?id=34668cee8ac371c98657beffcff579b8", "/js/components.js": "/js/components.js?id=34668cee8ac371c98657beffcff579b8",
"/css/app.css": "/css/app.css?id=05445eb8fb6f79c539507896e268aa91", "/css/app.css": "/css/app.css?id=366d6978f145ae9341959edd6e69bed7",
"/fonts/vendor/bootstrap-icons/bootstrap-icons.woff2?92ea18a81d737146ff044ddb52010366": "/fonts/vendor/bootstrap-icons/bootstrap-icons.woff2?92ea18a81d737146ff044ddb52010366?id=9ad5aa740d7d5e6a0191b309b9b1986c", "/fonts/vendor/bootstrap-icons/bootstrap-icons.woff2?92ea18a81d737146ff044ddb52010366": "/fonts/vendor/bootstrap-icons/bootstrap-icons.woff2?92ea18a81d737146ff044ddb52010366?id=9ad5aa740d7d5e6a0191b309b9b1986c",
"/fonts/vendor/bootstrap-icons/bootstrap-icons.woff?1295669cd4e305c97f2cfc16d56614b8": "/fonts/vendor/bootstrap-icons/bootstrap-icons.woff?1295669cd4e305c97f2cfc16d56614b8?id=d1af9af6052c51b169be6c628ae99bb5", "/fonts/vendor/bootstrap-icons/bootstrap-icons.woff?1295669cd4e305c97f2cfc16d56614b8": "/fonts/vendor/bootstrap-icons/bootstrap-icons.woff?1295669cd4e305c97f2cfc16d56614b8?id=d1af9af6052c51b169be6c628ae99bb5",
"/fonts/vendor/@fortawesome/fontawesome-free/webfa-brands-400.eot?23f19bb08961f37aaf692ff943823453": "/fonts/vendor/@fortawesome/fontawesome-free/webfa-brands-400.eot?23f19bb08961f37aaf692ff943823453?id=0868992b7c56298026b76b22c534eab9", "/fonts/vendor/@fortawesome/fontawesome-free/webfa-brands-400.eot?23f19bb08961f37aaf692ff943823453": "/fonts/vendor/@fortawesome/fontawesome-free/webfa-brands-400.eot?23f19bb08961f37aaf692ff943823453?id=0868992b7c56298026b76b22c534eab9",

View File

@ -390,14 +390,13 @@ window.OrderModule = function () {
stepEnable.push(stepName); stepEnable.push(stepName);
if(Order.getData()['orderStatus']==stepName){ if(Order.getData()['orderStatus']==stepName){
stepActive=stepName; stepActive=stepName;
/* // Az invoice és send lépéseknél az autoCallback hívja meg az initView*-ot,
debug('initView'+stepName.capitalize()); // ezért a globális onAfterActivate-et üresen hagyjuk különben kétszer hívódna.
if(stepName!=='invoice' && stepName!=='send'){ if(stepName!=='invoice' && stepName!=='send'){
stepOnAfterActivate=this['initView'+stepName.capitalize()]; stepOnAfterActivate=this['initView'+stepName.capitalize()];
}else{ }else{
stepOnAfterActivate=function (){}; stepOnAfterActivate=function (){};
} }
*/
break; break;
} }
} }
@ -1360,6 +1359,10 @@ solution before template
//@note: be kell kapcsolni az elonezetet //@note: be kell kapcsolni az elonezetet
root.Stepper.addCompleted('invoice'); root.Stepper.addCompleted('invoice');
root.Stepper.addEnabled('invoice'); root.Stepper.addEnabled('invoice');
// Érvénytelen szállítási dátum esetén Toast figyelmeztetés megjelenítése.
root.checkDeliveryDateToast();
let CSSSelectorForm='.orderParameterForm'; let CSSSelectorForm='.orderParameterForm';
//console.log(Opts.URL.getDataTableContentProductList);change //console.log(Opts.URL.getDataTableContentProductList);change
debug((CSSSelectorForm+' .fa-search')); debug((CSSSelectorForm+' .fa-search'));
@ -1700,6 +1703,9 @@ solution before template
newWin.print(); newWin.print();
newWin.close(); newWin.close();
}); });
// Érvénytelen szállítási dátum esetén Toast figyelmeztetés megjelenítése.
root.checkDeliveryDateToast();
} }
this.resetOrder=function () { this.resetOrder=function () {
@ -1727,11 +1733,100 @@ solution before template
APP.OrderModule.Stepper.setEnabled(value); APP.OrderModule.Stepper.setEnabled(value);
APP.OrderModule.Stepper.setCompleted(value); APP.OrderModule.Stepper.setCompleted(value);
}); });
// A szállítási dátum a küldés pillanatában már érvénytelen (pl. piszkozat időközbeni változás).
if(data && data.errorCode === 'INVALID_DELIVERY_DATE'){
root.showInvalidDeliveryDate(data.suggestedDeliveryDate || null);
}
}
/**
* Ellenőrzi a szállítási dátumot és ha érvénytelen, Toast üzenetet jelenít meg.
* Csak a productSelect és invoice nézetekből hívható a send nézetben a toggleEnableSend végzi ezt.
*/
this.checkDeliveryDateToast=function () {
if (!Order.id) {
return;
}
let ajaxUrl=Opts.URL.checkReadyToSend.replace('%id%',Order.id);
$.ajax({
url:ajaxUrl,
success:function (data) {
if(data && data.success && data.why && $.inArray('deliveryDate', data.why) !== -1){
let suggested = data.suggestedDeliveryDate || null;
let suggestedHuman = suggested ? suggested.replaceAll('-', '.') + '.' : null;
let message = 'A kiválasztott szállítási dátum már nem megfelelő.';
if(suggestedHuman){
message += ' Javasolt új dátum: ' + suggestedHuman;
}
Toast.create({ title: "Érvénytelen szállítási dátum", message: message, status: TOAST_STATUS.WARNING, timeout: 8000 });
}
}
});
}
/**
* Megjeleníti a send nézeten az érvénytelen szállítási dátum figyelmeztetést a javasolt új dátummal,
* és letiltja a "MEGRENDELÉST ELKÜLDÖM" gombot, amíg a felhasználó nem hagyja jóvá az új dátumot.
*/
this.showInvalidDeliveryDate=function (suggested) {
let suggestedHuman = suggested ? suggested.replaceAll('-', '.') + '.' : '-';
$('.suggestedDeliveryDatePlace').text(suggestedHuman);
$('.invalidDeliveryDateWarning').data('suggested-date', suggested || '');
$('.invalidDeliveryDateWarning').removeClass('d-none').addClass('d-flex');
// Amíg a felhasználó nem hagyja jóvá az új dátumot, az elküldés gomb se ne látsszon, se ne legyen aktív.
$('.buttonOrderSend').addClass('d-none').prop('disabled', true);
// A javaslat meglététől függően állítjuk be a jóváhagyó gombot.
if(suggested){
$('.suggestedDeliveryDateBlock').removeClass('d-none');
$('.buttonApproveSuggestedDate').text('Jóváhagyom és módosítom a dátumot');
}else{
$('.suggestedDeliveryDateBlock').addClass('d-none');
$('.buttonApproveSuggestedDate').text('Új szállítási dátum választása');
}
let message = 'A kiválasztott szállítási dátum már nem megfelelő.';
if(suggested){
message += ' Javasolt új dátum: ' + suggestedHuman;
}
Toast.create({ title: "Érvénytelen szállítási dátum", message: message, status: TOAST_STATUS.WARNING, timeout: 8000 });
}
/**
* A felhasználó jóváhagyja a javasolt új szállítási dátumot: visszanavigálunk a paraméter
* (naptár) lépéshez, ahol az új dátum lesz előválasztva, így onnan újra végigvihető a folyamat.
*/
this.approveSuggestedDeliveryDate=function () {
let suggested = $('.invalidDeliveryDateWarning').data('suggested-date');
if(suggested){
Order.setData({deliveryDate: suggested});
root.setDeliveryDate(suggested);
}
$('.invalidDeliveryDateWarning').removeClass('d-flex').addClass('d-none');
// A küldés gombot csak akkor engedjük újra, ha a felhasználó a paraméter lépésben
// érvényes dátumot választ ezért itt visszanavigálunk a naptárhoz.
$('.buttonOrderSend').removeClass('d-none').prop('disabled', false);
root.Stepper.setActive('parameter');
} }
this.initViewSend=function () { this.initViewSend=function () {
console.log('initViewSend called 2'); console.log('initViewSend called 2');
root.Stepper.setOpts({onAfterLoad:function (){}}); root.Stepper.setOpts({onAfterLoad:function (){}});
// Figyelmeztető panel alaphelyzetbe állítása és a jóváhagyás gomb bekötése.
// A küldés gombot szándékosan NEM tesszük láthatóvá itt a toggleEnableSend()
// aszinkron checkReadyToSend válasza fogja a végső állapotot beállítani.
$('.invalidDeliveryDateWarning').removeClass('d-flex').addClass('d-none');
$('.buttonOrderSend').addClass('d-none').prop('disabled', true);
$('.buttonApproveSuggestedDate').off('click');
$('.buttonApproveSuggestedDate').on('click', root.approveSuggestedDeliveryDate);
// Proaktív UX-ellenőrzés: a send nézet betöltésekor jelezzük, ha a szállítási dátum
// időközben érvénytelenné vált, és felajánljuk a következő érvényes dátumot.
root.toggleEnableSend();
$('.buttonOrderSend').off('click'); $('.buttonOrderSend').off('click');
$('.buttonOrderSend').on('click',function () { $('.buttonOrderSend').on('click',function () {
console.log('Send buton clicked'); console.log('Send buton clicked');
@ -1845,28 +1940,35 @@ send
} }
this.toggleEnableSend=function () { this.toggleEnableSend=function () {
console.log('toggleEnableSend called'); console.log('toggleEnableSend called');
// Ha még nincs mentett rendelés ID (pl. új rendelés létrehozásakor a supplier lépésnél),
// a checkReadyToSend hívás felesleges és hibát okozna (/order/undefined/checkReadyToSend).
if (!Order.id) {
return;
}
let ajaxUrl=Opts.URL.checkReadyToSend.replace('%id%',Order.id); let ajaxUrl=Opts.URL.checkReadyToSend.replace('%id%',Order.id);
$.ajax({ $.ajax({
url:ajaxUrl, url:ajaxUrl,
success:function (data) { success:function (data) {
if(data.success){ console.log('toggleEnableSend response:', data);
if(data.readyToSend){ // Alapértelmezetten elrejtjük a figyelmeztetést csak konkrét deliveryDate hiba esetén mutatjuk.
root.Stepper.addCompleted('send'); $('.invalidDeliveryDateWarning').removeClass('d-flex').addClass('d-none');
/*
root.Stepper.addCompleted('invoice'); if(data && data.success){
root.Stepper.addEnabled('invoice'); // A szállítási dátum időközben érvénytelenné vált figyelmeztetés a javasolt dátummal.
*/ if(data.why && $.inArray('deliveryDate', data.why) !== -1){
root.Stepper.addEnabled('send'); root.showInvalidDeliveryDate(data.suggestedDeliveryDate || null);
}else if(data.readyToSend){
$('.buttonOrderSend').removeClass('d-none').prop('disabled', false);
}else{ }else{
root.Stepper.unsetCompleted('send'); $('.buttonOrderSend').addClass('d-none').prop('disabled', true);
/*
root.Stepper.unsetCompleted('invoice');
root.Stepper.unsetEnabled('invoice');
*/
root.Stepper.unsetEnabled('send');
} }
} }
console.log(data); },
error:function (xhr) {
// AJAX hiba esetén (pl. 419, 500) a figyelmeztetés rejtve marad, gomb letiltva.
console.log('toggleEnableSend AJAX error:', xhr.status, xhr.responseText);
$('.invalidDeliveryDateWarning').removeClass('d-flex').addClass('d-none');
$('.buttonOrderSend').addClass('d-none').prop('disabled', true);
} }
}); });
} }

View File

@ -1,5 +1,13 @@
<div class="d-flex justify-content-center invalidDeliveryDateWarning d-none mb-3">
<div class="alert alert-warning text-center w-100" role="alert">
<h5 class="alert-heading">A szállítási dátum már nem megfelelő</h5>
<p class="mb-2">A korábban kiválasztott szállítási dátum időközben érvénytelenné vált a szállítói korlátozások miatt.</p>
<p class="mb-3 suggestedDeliveryDateBlock">Javasolt új szállítási dátum: <strong class="suggestedDeliveryDatePlace">-</strong></p>
<button type="button" class="btn btn-warning buttonApproveSuggestedDate">Jóváhagyom és módosítom a dátumot</button>
</div>
</div>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<button class="btn btn-lg btn-success buttonOrderSend">A MEGRENDELÉST ELKÜLDÖM <button class="btn btn-lg btn-success buttonOrderSend d-none" disabled>A MEGRENDELÉST ELKÜLDÖM
</button> </button>
<button class="btn btn-lg btn-primary buttonOrderSending d-none">Küldés folyamatban <button class="btn btn-lg btn-primary buttonOrderSending d-none">Küldés folyamatban

View File

@ -1,44 +0,0 @@
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different classes or traits.
|
*/
uses(Tests\TestCase::class)->in('Feature', 'Unit');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation class at any time.
|
*/
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can register helper functions.
|
*/
function something()
{
// ..
}