Einmalprodukte (Normalsortiment, show_on 3) können jetzt schon beim Abo-Abschluss hinzugefügt werden – im Portal-Wizard (Abschnitt im Upgrade-Schritt) und in allen drei Sales-Center-Abo-Strecken (eigene Produktliste). Variante A: Zeilen laufen als Cart-Option abo_addon mit durch den Checkout, werden sofort bezahlt und mit der Erstlieferung verschickt (Rechnung/Lieferschein gruppieren automatisch). - Checkout persistiert is_abo_addon (CheckoutRepository, PaymentHelper) - createAboItems/ensureUserAboItemsFromLatestOrder überspringen Addons (sonst würden Einmalprodukte dauerhaft wiederkehren); amount ohne Addon-Anteil; aboHasBaseProduct ignoriert Addon-Zeilen - CreateAboReplacementOrder propagiert is_abo_addon in Ersatzbestellungen - iq-shopping-cart.js: zweite Datatable + abo_addon-Flag-Weitergabe - i18n: neue abo.onetime_create_*-Keys (de/en/es/fr); zusätzlich es/fr komplett synchronisiert (abo-Kartenblock, navigation, payment, weborder inkl. Entfernung PAYONE-Altlasten, withdrawal.php neu) - Tests: AboCreateOneTimeItemsTest, AboCreateOneTimeCartTest, AboPortalCreateOneTimeTest (17 neue, 122 Tests gesamt grün) Plan: dev/2026-07-21-abo-einmalprodukte-anlage/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
317 lines
No EOL
12 KiB
JavaScript
Executable file
317 lines
No EOL
12 KiB
JavaScript
Executable file
|
|
var IqShoppingCart = {
|
|
table: "#datatables-order-list",
|
|
// Zweite Produktliste (Abo-Anlage): Einmalprodukte, deren Cart-Zeilen die Option abo_addon tragen
|
|
table_onetime: "#datatables-onetime-list",
|
|
btn_add: '.add-product-basket',
|
|
btn_remove: '.remove-product-basket',
|
|
card_holder: '#holder_html_view_card',
|
|
comp_holder: '#holder_html_view_comp_product',
|
|
is_for: null,
|
|
url: null,
|
|
btn_clear: '#clear-products-basket',
|
|
modal: null,
|
|
oTable: null,
|
|
table_input: '.table-input-event-onchange',
|
|
cart_input: '.cart-input-event-onchange',
|
|
cart_add: '.cart-add-event',
|
|
cart_remove: '.cart-remove-event',
|
|
remove_item: '.remove_item_form_cart',
|
|
shipping_state: '#change_shipping_state',
|
|
comp_products: 'switchers-comp-product',
|
|
count_comp_products: 'count_comp_products',
|
|
shipping_is_for: 'shipping_is_for',
|
|
is_abo: 'is_abo',
|
|
|
|
|
|
init: function () {
|
|
var _self = this;
|
|
_self.url = $(_self.table).data('url');
|
|
_self.is_for = $('input[name="'+_self.shipping_is_for+'"]').val();
|
|
_self.is_abo = $('input[name="'+_self.is_abo+'"]').val();
|
|
_self.showInit();
|
|
$(_self.shipping_state).on('change', function(){
|
|
_self.update_shipping_state($(this));
|
|
});
|
|
return _self;
|
|
},
|
|
setDatabase: function (oTable){
|
|
var _self = this;
|
|
_self.oTable = oTable;
|
|
},
|
|
reInit: function (){
|
|
var _self = this;
|
|
[_self.table, _self.table_onetime].forEach(function(tableSel){
|
|
$(tableSel).find(_self.btn_add).off('click').on('click', function(){
|
|
_self.add_product($(this), tableSel)
|
|
});
|
|
|
|
$(tableSel).find(_self.btn_remove).off('click').on('click', function(){
|
|
_self.remove_product($(this), tableSel)
|
|
});
|
|
$(tableSel).find(_self.table_input).off('change').on('change', function(){
|
|
_self.update_input_table($(this), tableSel);
|
|
});
|
|
});
|
|
},
|
|
isAddonElement: function (_obj){
|
|
return _obj.data('abo-addon') == 1;
|
|
},
|
|
qtyInputName: function (product_id, isAddon){
|
|
return (isAddon ? 'onetime_qty_' : 'product_qty_') + product_id;
|
|
},
|
|
showInit: function (){
|
|
var _self = this;
|
|
$(_self.btn_clear).on('click', function (){
|
|
_self.performRequest({action: 'clearCart'})
|
|
.done(_self.locationReload)
|
|
});
|
|
$(_self.cart_input).on('change', function(){
|
|
_self.update_input_cart($(this));
|
|
});
|
|
$(_self.cart_add).off('click').on('click', function(){
|
|
_self.change_cart_qty($(this), 1);
|
|
});
|
|
$(_self.cart_remove).off('click').on('click', function(){
|
|
_self.change_cart_qty($(this), -1);
|
|
});
|
|
$(_self.remove_item).on('click', function(event){
|
|
event.preventDefault();
|
|
_self.update_cart_database($(this).data('product-id'), 0, _self.isAddonElement($(this)));
|
|
});
|
|
if(_self.is_for === 'me' || _self.is_for === 'abo-me'){
|
|
$('input[name^="'+_self.comp_products+'"]').on('change', function(){
|
|
_self.update_comp_product($(this));
|
|
});
|
|
}
|
|
},
|
|
update_shipping_state : function (_obj){
|
|
var _self = this;
|
|
var id = parseInt(_obj.val());
|
|
var is_for = _obj.data('is-for');
|
|
_self.performRequest({shipping_country_id: id, shipping_is_for: is_for, action: 'updateShippingCountry'})
|
|
.done(_self.refreshItemsAndView);
|
|
},
|
|
update_input_table: function (_obj, tableSel){
|
|
var _self = this;
|
|
var qty = parseInt(_obj.val());
|
|
qty = _self.checkNumber(qty);
|
|
_obj.val(qty);
|
|
_self.update_cart(_obj.data('product-id'), qty, _self.isAddonElement(_obj));
|
|
},
|
|
update_input_cart: function (_obj){
|
|
var _self = this;
|
|
var qty = parseInt(_obj.val());
|
|
qty = _self.checkNumber(qty);
|
|
_obj.val(qty);
|
|
_self.update_cart_database(_obj.data('product-id'), qty, _self.isAddonElement(_obj));
|
|
},
|
|
/**
|
|
* Plus-/Minus-Buttons im Warenkorb: Menge nicht optimistisch erhöhen, sondern
|
|
* erst nach Server-Antwort (Re-Render) anzeigen. So bleibt bei einem Stop
|
|
* (z. B. Maximalgewicht) der Zähler stehen und es erscheint ein Inline-Hinweis.
|
|
*/
|
|
change_cart_qty: function (_obj, delta){
|
|
var _self = this;
|
|
var productId = _obj.data('product-id');
|
|
var isAddon = _self.isAddonElement(_obj);
|
|
var input = $(_self.card_holder).find('.cart-input-event-onchange[data-product-id="'+productId+'"]').filter(function(){
|
|
return IqShoppingCart.isAddonElement($(this)) === isAddon;
|
|
});
|
|
var qty = _self.checkNumber(parseInt(input.val(), 10) + delta);
|
|
_self.update_cart_database(productId, qty, isAddon);
|
|
},
|
|
update_comp_product: function (_obj){
|
|
var _self = this;
|
|
_self.performRequest({comp_product_id: _obj.val(), comp_num: _obj.data('comp_num'), count_comp_products: $('input[name="'+_self.count_comp_products+'"]').val(), action: 'updateCompProduct'})
|
|
.done(_self.refreshItemsAndView);
|
|
},
|
|
add_product: function (_obj, tableSel){
|
|
var _self = this;
|
|
var isAddon = _self.isAddonElement(_obj);
|
|
var input = $(tableSel || _self.table).find('input[name="'+_self.qtyInputName(_obj.data('product-id'), isAddon)+'"]');
|
|
var qty = parseInt(input.val()) + 1;
|
|
qty = _self.checkNumber(qty);
|
|
input.val(qty);
|
|
_self.update_cart(_obj.data('product-id'), qty, isAddon);
|
|
},
|
|
remove_product: function (_obj, tableSel){
|
|
var _self = this;
|
|
var isAddon = _self.isAddonElement(_obj);
|
|
var input = $(tableSel || _self.table).find('input[name="'+_self.qtyInputName(_obj.data('product-id'), isAddon)+'"]');
|
|
var qty = parseInt(input.val()) - 1;
|
|
if(qty < 0){
|
|
qty = 0;
|
|
}
|
|
input.val(qty);
|
|
_self.update_cart(_obj.data('product-id'), qty, isAddon);
|
|
},
|
|
update_cart_database: function (product_id, qty, isAddon){
|
|
var _self = this;
|
|
_self.performRequest({product_id: product_id, qty: qty, abo_addon: isAddon ? 1 : 0, action: 'updateCart'})
|
|
.done(_self.refreshDatabaseAndView);
|
|
},
|
|
update_cart: function (product_id, qty, isAddon){
|
|
var _self = this;
|
|
_self.performRequest({product_id: product_id, qty: qty, abo_addon: isAddon ? 1 : 0, action: 'updateCart'})
|
|
.done(_self.refreshItemsAndView);
|
|
},
|
|
/**
|
|
* Liefertag-Info (wenn keine Warnung) bzw. Warnung (Partner-Center Abo, < 20 Tage): nach jedem AJAX-Warenkorb-Update,
|
|
* da eingebettete Scripts in .html() nicht ausgeführt werden.
|
|
*/
|
|
updateAboIntervalWarning: function () {
|
|
var warningTemplate = window.aboIntervalWarningTemplate;
|
|
var infoTemplate = window.aboIntervalInfoTemplate;
|
|
if (!warningTemplate && !infoTemplate) {
|
|
return;
|
|
}
|
|
var select = document.getElementById('abo_interval_select');
|
|
if (!select) {
|
|
return;
|
|
}
|
|
var warning = document.getElementById('abo_interval_warning');
|
|
var info = document.getElementById('abo_interval_info');
|
|
var option = select.options[select.selectedIndex];
|
|
if (!option) {
|
|
return;
|
|
}
|
|
var days = parseInt(option.getAttribute('data-days'), 10);
|
|
var date = option.getAttribute('data-date') || '';
|
|
if (isNaN(days)) {
|
|
return;
|
|
}
|
|
var showWarning = days < 20;
|
|
if (warning && warningTemplate) {
|
|
if (showWarning) {
|
|
warning.innerHTML =
|
|
'<i class="fa fa-exclamation-triangle"></i> ' +
|
|
warningTemplate.replace('__DAYS__', days).replace('__DATE__', date);
|
|
warning.classList.remove('d-none');
|
|
} else {
|
|
warning.classList.add('d-none');
|
|
}
|
|
}
|
|
if (info && infoTemplate) {
|
|
if (showWarning) {
|
|
info.innerHTML = '';
|
|
info.classList.add('d-none');
|
|
} else {
|
|
info.innerHTML =
|
|
'<i class="fa fa-info-circle"></i> ' +
|
|
infoTemplate.replace('__DAYS__', days).replace('__DATE__', date);
|
|
info.classList.remove('d-none');
|
|
}
|
|
}
|
|
},
|
|
|
|
showCartError: function (message){
|
|
var _self = this;
|
|
var errorEl = document.getElementById('insert_show_error_message');
|
|
if (!errorEl) {
|
|
errorEl = document.createElement('div');
|
|
errorEl.id = 'insert_show_error_message';
|
|
errorEl.className = 'alert alert-danger mt-2';
|
|
var cartContent = document.getElementById('cartContent');
|
|
if (cartContent) {
|
|
cartContent.insertBefore(errorEl, cartContent.firstChild);
|
|
}
|
|
}
|
|
errorEl.textContent = message;
|
|
},
|
|
refreshItemsAndView: function (data){
|
|
var _self = IqShoppingCart;
|
|
if (data && data.response === false && data.message) {
|
|
_self.showCartError(data.message);
|
|
return;
|
|
}
|
|
$(_self.card_holder).html(data.html_card);
|
|
$(_self.comp_holder).html(data.html_comp);
|
|
_self.showInit();
|
|
_self.updateAboIntervalWarning();
|
|
|
|
},
|
|
refreshDatabaseAndView: function (data) {
|
|
var _self = IqShoppingCart;
|
|
if (data && data.response === false && data.message) {
|
|
_self.showCartError(data.message);
|
|
return;
|
|
}
|
|
$(_self.card_holder).html(data.html_card);
|
|
$(_self.comp_holder).html(data.html_comp);
|
|
|
|
var isAddon = data.data && data.data.abo_addon == 1;
|
|
var tableSel = isAddon ? _self.table_onetime : _self.table;
|
|
var input = $(tableSel).find('input[name="'+_self.qtyInputName(data.data.product_id, isAddon)+'"]');
|
|
input.val(data.data.qty);
|
|
_self.showInit();
|
|
_self.updateAboIntervalWarning();
|
|
},
|
|
locationReload : function(){
|
|
//location.reload();
|
|
location.replace(location.href);
|
|
},
|
|
refreshDatabaseRefreshAndView : function (data){
|
|
var _self = IqShoppingCart;
|
|
$(_self.card_holder).html(data.html_card);
|
|
$(_self.comp_holder).html(data.html_comp);
|
|
|
|
_self.showInit();
|
|
_self.updateAboIntervalWarning();
|
|
_self.oTable.draw();
|
|
},
|
|
checkNumber : function(number){
|
|
if(number < 0 || isNaN(number)){
|
|
return 0;
|
|
}
|
|
if(number >= 999){
|
|
return 999;
|
|
}
|
|
return number;
|
|
},
|
|
performRequest : function(data) {
|
|
var _self = this;
|
|
var url = _self.url,
|
|
contentType = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
|
|
data.shipping_is_for = _self.is_for;
|
|
data.is_abo = _self.is_abo;
|
|
|
|
console.log(data);
|
|
console.log(url);
|
|
|
|
return $.ajax({
|
|
url: url,
|
|
data: data,
|
|
type: "POST",
|
|
dataType: "json",
|
|
cache: false,
|
|
contentType: contentType,
|
|
encode: true,
|
|
headers: {
|
|
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
|
}
|
|
})
|
|
.done(function (data) {
|
|
|
|
console.log('performRequest');
|
|
console.log(data);
|
|
|
|
|
|
|
|
}).fail(function (jqXHR, textStatus, errorThrown) {
|
|
console.log(jqXHR);
|
|
console.log(jqXHR.responseText);
|
|
console.log(textStatus);
|
|
console.log(errorThrown);
|
|
console.log("Sorry, there was a problem!");
|
|
});
|
|
}
|
|
};
|
|
|
|
$(function () {
|
|
$(document).on('change', '#abo_interval_select', function () {
|
|
IqShoppingCart.updateAboIntervalWarning();
|
|
});
|
|
IqShoppingCart.updateAboIntervalWarning();
|
|
}); |