This commit is contained in:
2026-03-28 11:21:41 +01:00
parent 913bad66bd
commit d366a58d28
6 changed files with 271 additions and 85 deletions

View File

@@ -5,7 +5,6 @@ class ShopBasketController
{ {
private const ORDER_SUBMIT_TOKEN_SESSION_KEY = 'order-submit-token'; private const ORDER_SUBMIT_TOKEN_SESSION_KEY = 'order-submit-token';
private const ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY = 'order-submit-last-order-id'; private const ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY = 'order-submit-last-order-id';
private const ORDER_SUBMIT_TOKEN_TTL = 1800; // 30 minutes
public static $title = [ public static $title = [
'mainView' => 'Koszyk' 'mainView' => 'Koszyk'
@@ -277,6 +276,19 @@ class ShopBasketController
exit; exit;
} }
$existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] )
? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ]
: 0;
if ( $existingOrderId > 0 )
{
$existingOrderHash = $this->orderRepository->findHashById( $existingOrderId );
if ( $existingOrderHash )
{
header( 'Location: /zamowienie/' . $existingOrderHash );
exit;
}
}
$client = \Shared\Helpers\Helpers::get_session( 'client' ); $client = \Shared\Helpers\Helpers::get_session( 'client' );
$orderSubmitToken = $this->createOrderSubmitToken(); $orderSubmitToken = $this->createOrderSubmitToken();
@@ -296,26 +308,23 @@ class ShopBasketController
public function basketSave() public function basketSave()
{ {
$basket = \Shared\Helpers\Helpers::get_session( 'basket' ); $orderSubmitToken = (string)\Shared\Helpers\Helpers::get( 'order_submit_token', true );
$existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] ) ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] : 0; $existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] ) ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] : 0;
if ( ( !is_array( $basket ) || empty( $basket ) ) && $existingOrderId > 0 )
{
$existingOrderHash = $this->orderRepository->findHashById( $existingOrderId );
if ( $existingOrderHash )
{
$this->logOrder( 'Double-submit detected, redirecting to existing order id=' . $existingOrderId );
header( 'Location: /zamowienie/' . $existingOrderHash );
exit;
}
}
$orderSubmitToken = (string)\Shared\Helpers\Helpers::get( 'order_submit_token', true );
if ( !$this->isValidOrderSubmitToken( $orderSubmitToken ) ) if ( !$this->isValidOrderSubmitToken( $orderSubmitToken ) )
{ {
$this->logOrder( 'Token validation failed. formToken=' . ($orderSubmitToken ?: '(empty)') ); if ( $existingOrderId > 0 )
{
$existingOrderHash = $this->orderRepository->findHashById( $existingOrderId );
if ( $existingOrderHash )
{
header( 'Location: /zamowienie/' . $existingOrderHash );
exit;
}
}
\Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) ); \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) );
header( 'Location: /koszyk-podsumowanie' ); header( 'Location: /koszyk' );
exit; exit;
} }
@@ -358,7 +367,7 @@ class ShopBasketController
} }
catch ( \Exception $e ) catch ( \Exception $e )
{ {
$this->logOrder( 'createFromBasket exception: ' . $e->getMessage() ); error_log( '[basketSave] createFromBasket exception: ' . $e->getMessage() );
\Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) ); \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) );
header( 'Location: /koszyk' ); header( 'Location: /koszyk' );
exit; exit;
@@ -391,7 +400,6 @@ class ShopBasketController
} }
else else
{ {
$this->logOrder( 'createFromBasket returned falsy order_id. client_id=' . ($client['id'] ?? 'guest') . ' email=' . \Shared\Helpers\Helpers::get( 'email', true ) );
\Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) ); \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) );
header( 'Location: /koszyk' ); header( 'Location: /koszyk' );
exit; exit;
@@ -438,6 +446,79 @@ class ShopBasketController
] ); ] );
} }
public function basketUpdateCustomFields()
{
$basket = \Shared\Helpers\Helpers::get_session( 'basket' );
$product_code = \Shared\Helpers\Helpers::get( 'product_code' );
if ( !isset( $basket[ $product_code ] ) )
{
echo json_encode( [ 'result' => 'error', 'message' => 'Pozycja nie istnieje w koszyku' ] );
exit;
}
$position = $basket[ $product_code ];
$new_custom_fields = [];
$custom_fields_raw = \Shared\Helpers\Helpers::get( 'custom_field' );
if ( is_array( $custom_fields_raw ) )
{
foreach ( $custom_fields_raw as $field_id => $value )
{
$new_custom_fields[ (int)$field_id ] = $value;
}
}
$productRepo = new \Domain\Product\ProductRepository( $GLOBALS['mdb'] );
$missing_fields = [];
foreach ( $new_custom_fields as $field_id => $value )
{
$field_meta = $productRepo->findCustomFieldCached( $field_id );
if ( $field_meta && (int)$field_meta['is_required'] === 1 && trim( $value ) === '' )
{
$missing_fields[] = $field_meta['name'];
}
}
if ( count( $missing_fields ) > 0 )
{
echo json_encode( [ 'result' => 'error', 'message' => 'Wypełnij wymagane pola: ' . implode( ', ', $missing_fields ) ] );
exit;
}
$attributes_implode = '';
if ( isset( $position['attributes'] ) && is_array( $position['attributes'] ) && count( $position['attributes'] ) > 0 )
{
$attributes_implode = implode( '|', $position['attributes'] );
}
$message = isset( $position['message'] ) ? $position['message'] : '';
$new_product_code = md5( $position['product-id'] . $attributes_implode . $message . json_encode( $new_custom_fields ) );
if ( $new_product_code === $product_code )
{
$basket[ $product_code ]['custom_fields'] = $new_custom_fields;
}
elseif ( isset( $basket[ $new_product_code ] ) )
{
$basket[ $new_product_code ]['quantity'] += $position['quantity'];
unset( $basket[ $product_code ] );
}
else
{
$position['custom_fields'] = $new_custom_fields;
$basket[ $new_product_code ] = $position;
unset( $basket[ $product_code ] );
}
$basket = ( new \Domain\Promotion\PromotionRepository( $GLOBALS['mdb'] ) )->findPromotion( $basket );
\Shared\Helpers\Helpers::set_session( 'basket', $basket );
echo json_encode( [ 'result' => 'ok' ] );
exit;
}
private function jsonBasketResponse( $basket, $coupon, $lang_id, $basket_transport_method_id ) private function jsonBasketResponse( $basket, $coupon, $lang_id, $basket_transport_method_id )
{ {
global $settings; global $settings;
@@ -463,23 +544,8 @@ class ShopBasketController
private function createOrderSubmitToken() private function createOrderSubmitToken()
{ {
$existingTokenData = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] ) ? $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] : null;
if ( is_array( $existingTokenData ) && !empty( $existingTokenData['token'] ) && !empty( $existingTokenData['created_at'] ) )
{
$age = time() - (int)$existingTokenData['created_at'];
if ( $age < self::ORDER_SUBMIT_TOKEN_TTL )
{
\Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY );
return $existingTokenData['token'];
}
}
$token = $this->generateOrderSubmitToken(); $token = $this->generateOrderSubmitToken();
\Shared\Helpers\Helpers::set_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY, [ \Shared\Helpers\Helpers::set_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY, $token );
'token' => $token,
'created_at' => time()
] );
\Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ); \Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY );
return $token; return $token;
@@ -502,26 +568,10 @@ class ShopBasketController
if ( !$token ) if ( !$token )
return false; return false;
$tokenData = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] ) ? $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] : null; $sessionToken = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] ) ? (string)$_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] : '';
if ( !$sessionToken )
if ( is_string( $tokenData ) )
{
$sessionToken = $tokenData;
if ( !$sessionToken )
return false;
return function_exists( 'hash_equals' ) ? hash_equals( $sessionToken, $token ) : $sessionToken === $token;
}
if ( !is_array( $tokenData ) || empty( $tokenData['token'] ) || empty( $tokenData['created_at'] ) )
return false; return false;
$age = time() - (int)$tokenData['created_at'];
if ( $age > self::ORDER_SUBMIT_TOKEN_TTL )
return false;
$sessionToken = (string)$tokenData['token'];
if ( function_exists( 'hash_equals' ) ) if ( function_exists( 'hash_equals' ) )
return hash_equals( $sessionToken, $token ); return hash_equals( $sessionToken, $token );
@@ -532,12 +582,4 @@ class ShopBasketController
{ {
\Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY ); \Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY );
} }
private function logOrder( $message )
{
$logDir = $_SERVER['DOCUMENT_ROOT'] . '/logs';
$logFile = $logDir . '/logs-order-' . date( 'Y-m-d' ) . '.log';
$line = '[' . date( 'Y-m-d H:i:s' ) . '] ' . $message . PHP_EOL;
@file_put_contents( $logFile, $line, FILE_APPEND | LOCK_EX );
}
} }

View File

@@ -1,26 +1,52 @@
<? if ( $this -> custom_fields ) : ?> <? if ( $this -> custom_fields ) : ?>
<? foreach ( $this -> custom_fields as $key => $val ) : ?> <div class="custom-fields-display" data-product-code="<?= htmlspecialchars( $this->product_code ); ?>">
<? $custom_field = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCustomFieldCached( $key ); ?> <? foreach ( $this -> custom_fields as $key => $val ) : ?>
<? $field_type = !empty( $custom_field['type'] ) ? $custom_field['type'] : 'text'; ?> <? $custom_field = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCustomFieldCached( $key ); ?>
<? $field_type = !empty( $custom_field['type'] ) ? $custom_field['type'] : 'text'; ?>
<? if ( $field_type == 'text' ) : ?> <? if ( $field_type == 'text' ) : ?>
<div class="custom-field"> <div class="custom-field">
<div class="_name"> <div class="_name">
<?= htmlspecialchars( $custom_field['name'] ) . ':'; ?> <?= htmlspecialchars( $custom_field['name'] ) . ':'; ?>
</div>
<div class="_text">
<?= nl2br( htmlspecialchars( $val ) );?>
</div>
</div> </div>
<div class="_text"> <? elseif ( $field_type == 'image' && !empty( $val ) ) : ?>
<?= nl2br( htmlspecialchars( $val ) );?> <div class="custom-field">
<div class="_name">
<?= htmlspecialchars( $custom_field['name'] ) . ':'; ?>
</div>
<div class="_image">
<img src="<?= htmlspecialchars( $val );?>" alt="<?= htmlspecialchars( $custom_field['name'] );?>">
</div>
</div> </div>
<? endif; ?>
<? endforeach; ?>
<a href="#" class="btn btn-sm btn-default btn-edit-custom-fields">Edytuj personalizację</a>
</div>
<div class="custom-fields-edit" data-product-code="<?= htmlspecialchars( $this->product_code ); ?>" style="display: none;">
<? foreach ( $this -> custom_fields as $key => $val ) : ?>
<? $custom_field = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCustomFieldCached( $key ); ?>
<? $field_type = !empty( $custom_field['type'] ) ? $custom_field['type'] : 'text'; ?>
<? $is_required = !empty( $custom_field['is_required'] ) ? (int)$custom_field['is_required'] : 0; ?>
<div class="custom-field-edit-row" style="margin-bottom: 5px;">
<label>
<?= htmlspecialchars( $custom_field['name'] ); ?><?= $is_required ? ' <span style="color:red;">*</span>' : ''; ?>
</label>
<? if ( $field_type == 'text' ) : ?>
<input type="text" class="form-control form-control-sm" name="custom_field[<?= (int)$key; ?>]" value="<?= htmlspecialchars( $val ); ?>" <?= $is_required ? 'required' : ''; ?>>
<? elseif ( $field_type == 'image' ) : ?>
<input type="text" class="form-control form-control-sm" name="custom_field[<?= (int)$key; ?>]" value="<?= htmlspecialchars( $val ); ?>" placeholder="URL obrazka" <?= $is_required ? 'required' : ''; ?>>
<? endif; ?>
</div> </div>
<? elseif ( $field_type == 'image' && !empty( $val ) ) : ?> <? endforeach; ?>
<div class="custom-field"> <div style="margin-top: 5px;">
<div class="_name"> <a href="#" class="btn btn-sm btn-primary btn-save-custom-fields">Zapisz</a>
<?= htmlspecialchars( $custom_field['name'] ) . ':'; ?> <a href="#" class="btn btn-sm btn-default btn-cancel-custom-fields">Anuluj</a>
</div> </div>
<div class="_image"> </div>
<img src="<?= htmlspecialchars( $val );?>" alt="<?= htmlspecialchars( $custom_field['name'] );?>"> <? endif; ?>
</div>
</div>
<? endif; ?>
<? endforeach; ?>
<? endif;?>

View File

@@ -61,7 +61,8 @@
<hr> <hr>
<? endif; ?> <? endif; ?>
<?= \Shared\Tpl\Tpl::view( 'shop-basket/_partials/product-custom-fields', [ <?= \Shared\Tpl\Tpl::view( 'shop-basket/_partials/product-custom-fields', [
'custom_fields' => $position['custom_fields'] 'custom_fields' => $position['custom_fields'],
'product_code' => $position_hash
] ); ?> ] ); ?>
<? if ( $product['additional_message'] ):?> <? if ( $product['additional_message'] ):?>
<div class="basket-product-message"> <div class="basket-product-message">

View File

@@ -508,4 +508,62 @@
console.warn('#orlen_point_id nie został znaleziony.'); console.warn('#orlen_point_id nie został znaleziony.');
} }
}); });
// edycja personalizacji produktu w koszyku
$(document).on('click', '.btn-edit-custom-fields', function(e) {
e.preventDefault();
var $display = $(this).closest('.custom-fields-display');
var productCode = $display.data('product-code');
$display.hide();
$display.siblings('.custom-fields-edit[data-product-code="' + productCode + '"]').show();
});
$(document).on('click', '.btn-cancel-custom-fields', function(e) {
e.preventDefault();
var $edit = $(this).closest('.custom-fields-edit');
var productCode = $edit.data('product-code');
$edit.hide();
$edit.siblings('.custom-fields-display[data-product-code="' + productCode + '"]').show();
});
$(document).on('click', '.btn-save-custom-fields', function(e) {
e.preventDefault();
var $edit = $(this).closest('.custom-fields-edit');
var productCode = $edit.data('product-code');
var valid = true;
$edit.find('input[required]').each(function() {
if ($.trim($(this).val()) === '') {
$(this).css('border-color', 'red');
valid = false;
} else {
$(this).css('border-color', '');
}
});
if (!valid) {
alert('Wypełnij wszystkie wymagane pola');
return;
}
var formData = { product_code: productCode };
$edit.find('input[name^="custom_field"]').each(function() {
formData[$(this).attr('name')] = $(this).val();
});
$.ajax({
type: 'POST',
cache: false,
url: '/shopBasket/basket_update_custom_fields',
data: formData,
success: function(response) {
var data = jQuery.parseJSON(response);
if (data.result === 'ok') {
location.reload();
} else {
alert(data.message || 'Wystąpił błąd');
}
}
});
});
</script> </script>

View File

@@ -45,7 +45,8 @@
<hr> <hr>
<? endif; ?> <? endif; ?>
<?= \Shared\Tpl\Tpl::view( 'shop-basket/_partials/product-custom-fields', [ <?= \Shared\Tpl\Tpl::view( 'shop-basket/_partials/product-custom-fields', [
'custom_fields' => $position['custom_fields'] 'custom_fields' => $position['custom_fields'],
'product_code' => $position_hash
] ); ?> ] ); ?>
<? if ( $product['additional_message'] ):?> <? if ( $product['additional_message'] ):?>
<div class="basket-product-message"> <div class="basket-product-message">
@@ -112,4 +113,4 @@
<div class="alert alert-danger">Brak produktów w koszyku</div> <div class="alert alert-danger">Brak produktów w koszyku</div>
<? endif; ?> <? endif; ?>
</div> </div>
</div> </div>

View File

@@ -537,4 +537,62 @@
console.warn('#orlen_point_id nie został znaleziony.'); console.warn('#orlen_point_id nie został znaleziony.');
} }
}); });
</script>
// edycja personalizacji produktu w koszyku
$(document).on('click', '.btn-edit-custom-fields', function(e) {
e.preventDefault();
var $display = $(this).closest('.custom-fields-display');
var productCode = $display.data('product-code');
$display.hide();
$display.siblings('.custom-fields-edit[data-product-code="' + productCode + '"]').show();
});
$(document).on('click', '.btn-cancel-custom-fields', function(e) {
e.preventDefault();
var $edit = $(this).closest('.custom-fields-edit');
var productCode = $edit.data('product-code');
$edit.hide();
$edit.siblings('.custom-fields-display[data-product-code="' + productCode + '"]').show();
});
$(document).on('click', '.btn-save-custom-fields', function(e) {
e.preventDefault();
var $edit = $(this).closest('.custom-fields-edit');
var productCode = $edit.data('product-code');
var valid = true;
$edit.find('input[required]').each(function() {
if ($.trim($(this).val()) === '') {
$(this).css('border-color', 'red');
valid = false;
} else {
$(this).css('border-color', '');
}
});
if (!valid) {
alert('Wypełnij wszystkie wymagane pola');
return;
}
var formData = { product_code: productCode };
$edit.find('input[name^="custom_field"]').each(function() {
formData[$(this).attr('name')] = $(this).val();
});
$.ajax({
type: 'POST',
cache: false,
url: '/shopBasket/basket_update_custom_fields',
data: formData,
success: function(response) {
var data = jQuery.parseJSON(response);
if (data.result === 'ok') {
location.reload();
} else {
alert(data.message || 'Wystąpił błąd');
}
}
});
});
</script>