PageSourceSearch

https://tickets.kiddydome.ch/js/helper/cross-selling.js?1790753569

js kiddydome.ch collected 2026-10-06 12:55:56 UTC 33,649 bytes, 748 lines download raw bytes

1
2var crossSellingInstances = [];
3
4function addNewCrossSellingInstance(additionalInfo = false, crossSellingSelection = false, preparedCrossSelling = false, requirements = false) {
5    if (additionalInfo || crossSellingSelection || preparedCrossSelling || requirements) {
6        let newCrossSellingInstance = {
7            additionalInfo: {},
8            crossSellingSelection: {},
9            preparedCrossSelling: {},
10            requirements: {},
11        };
12
13        if (additionalInfo) {
14            newCrossSellingInstance.additionalInfo = additionalInfo;
15        }
16        if (crossSellingSelection) {
17            newCrossSellingInstance.crossSellingSelection = crossSellingSelection;
18        }
19        if (preparedCrossSelling) {
20            newCrossSellingInstance.preparedCrossSelling = preparedCrossSelling;
21        }
22        if (requirements) {
23            newCrossSellingInstance.requirements = requirements;
24        }
25
26        crossSellingInstances.push(newCrossSellingInstance);
27        return crossSellingInstances.length - 1;
28    }
29
30    return false;
31}
32
33function insertCrossSellingEdit(csInstanceIndex) {
34    var getParams = crossSellingInstances[csInstanceIndex].additionalInfo;
35    getParams.csInstanceIndex = csInstanceIndex;
36    let selection = crossSellingInstances[csInstanceIndex].crossSellingSelection;
37    if (
38        !(Object.keys(selection).length) // json is present but empty (to enable creating new cross-selling from backend)
39        || !('itemGroups' in selection) // json is not empty but also not valid cross-selling-config (to enable creating new cross-selling from backend)
40        || (selection.itemGroups.length <= 0) // the itemGroups have the db id attached
41        || ("id" in selection.itemGroups[0]) // the itemGroups have the db id attached
42    ) {
43        let xhttp = new XMLHttpRequest();
44        xhttp.onload = function() {
45            let externalContainer = crossSellingInstances[csInstanceIndex].externalContainer;
46            let loadingSpinners = externalContainer.querySelectorAll('.working-spinner');
47            for (spinner of loadingSpinners) {
48                spinner.remove();
49                DEBUG && console.log('spinner removed');
50            }
51            // let containerHtmlSelector = '#' + externalContainer.getAttribute('id');
52            let containerHtmlSelector = crossSellingInstances[csInstanceIndex].externalContainerSelector;
53            $(containerHtmlSelector).html(this.responseText + $(containerHtmlSelector).html()); // insert cs edit and execute js
54
55            // hide all other elements
56            let otherElements = externalContainer.querySelectorAll(':scope > *:not(.cross-selling-edit-container)'); // :scope matches the element querySelector was called on (though only for DOM API functions like this)
57            for (currentElement of otherElements) {
58                currentElement.classList.add('hidden');
59            }
60
61            // get height relevant data from style
62            let heightReferenceElement = crossSellingInstances[csInstanceIndex].externalHeightReferenceElement;
63            DEBUG && console.log('heightReferenceElement', heightReferenceElement);
64            let style = window.getComputedStyle(heightReferenceElement, null);
65            let computedHeight = style.getPropertyValue("height");
66            let paddingTop = style.getPropertyValue("padding-top");
67            let paddingBottom = style.getPropertyValue("padding-bottom");
68
69            // make insertContainer fill the whole externalContainer to enable the cs-edit-summary's "position: sticky"
70            var insertContainer = externalContainer.querySelector('.insert.cross-selling-edit');
71            DEBUG && console.log('insertContainer', insertContainer);
72            newHeight = parseFloat(computedHeight) - (parseFloat(paddingTop) + parseFloat(paddingBottom) + 60);
73            insertContainer.style.maxHeight = newHeight + 'px';
74            insertContainer.style.overflow = 'auto';
75        }
76        xhttp.open("GET", "/articles/cross-selling/crosssellingedit" + new QuerySearchParams(getParams), true);
77        xhttp.send();
78    } else {
79        document.querySelector('.insertCSEdit[cs-instance-index="' + csInstanceIndex + '"]').textContent = 'Cross-Selling kann nicht bearbeitet werden.';
80    }
81}
82
83function saveCrossSellingConfig(csInstanceIndex) {
84    LoadingSpinner.add(crossSellingInstances[csInstanceIndex].insertContainer.querySelector('.cs-edit-save'));
85    updateCrossSellingConfig(csInstanceIndex);
86    var params = {
87        ticketsID: crossSellingInstances[csInstanceIndex].additionalInfo.ticketsID,
88        ordersID: crossSellingInstances[csInstanceIndex].additionalInfo.ordersID,
89        crossSellingConfig: JSON.stringify(crossSellingInstances[csInstanceIndex].crossSellingConfig),
90    };
91    let xhttp = new XMLHttpRequest();
92    xhttp.onload = function() {
93        if (this.responseText == 'true') {
94            DEBUG && console.log('cross-selling config saved');
95            crossSellingInstances[csInstanceIndex].crossSellingSelection = JSON.parse(JSON.stringify(crossSellingInstances[csInstanceIndex].crossSellingConfig)); // in order to start with up-to-date selection when edit is called again
96            crossSellingInstances[csInstanceIndex].postSaveCallback(csInstanceIndex);
97            cancelCrossSellingEdit(csInstanceIndex);
98        } else {
99            console.log('tried to save cross-selling config:', this.responseText);
100            notification(this.responseText, "danger", "fas fa-exclamation-triangle", 3000);
101            let loadingSpinners = crossSellingInstances[csInstanceIndex].insertContainer.querySelectorAll('.working-spinner');
102            for (spinner of loadingSpinners) {
103                spinner.remove();
104            }
105        }
106    }
107    xhttp.open("POST", "/articles/cross-selling/crosssellingsave", true);
108    xhttp.setRequestHeader('Content-Type', 'application/json');
109    xhttp.send(JSON.stringify(params));
110}
111
112function cancelCrossSellingEdit(csInstanceIndex) {
113    var crossSellingEditContainer = crossSellingInstances[csInstanceIndex].crossSellingEditContainer;
114    crossSellingEditContainer.remove();
115    let externalContainer = crossSellingInstances[csInstanceIndex].externalContainer;
116    let otherElements = externalContainer.querySelectorAll(':scope > *:not(.cross-selling-edit-container)'); // here :scope matches the element querySelector was called on
117    for (currentElement of otherElements) {
118        currentElement.classList.remove('hidden');
119    }
120}
121
122//###############################################################################################################################################
123
124function checkMaxParticipantsExceeded(requirements, main, normal, companions) {
125    let relevantParticipants = 0;
126    relevantParticipants += requirements.participantsMaxIncludeMainParticipants ? main : 0;
127    relevantParticipants += requirements.participantsMaxIncludeNormalParticipants ? normal : 0;
128    relevantParticipants += requirements.participantsMaxIncludeCompanions ? companions : 0;
129    return relevantParticipants > requirements.participantsMax_all;
130}
131
132participantsCheckedIn_number = null;
133/**
134 * @returns [checkedInExceeded, participantsCheckedIn_number]
135 */
136function checkCheckedInParticipantsExceeded(csInstanceIndex, main, normal, companions) {
137    participantsCheckedIn_number = (typeof group !== 'undefined' && group.codes !== undefined) ? group.codes : participantsCheckedIn_number;
138    if (!isNumeric(participantsCheckedIn_number)) {
139        $.ajax({
140            url: "/pos/pos/get-ident-codes",
141            type: "POST",
142            data: {
143                ticket: crossSellingInstances[csInstanceIndex].additionalInfo.ticketsID,
144            },
145            dataType: "json",
146            async: false,
147            success: function(data) {
148                participantsCheckedIn_number = data.result.length;
149            },
150        });
151    }
152    let checkedInExceeded = (participantsCheckedIn_number > (main + normal + companions));
153    return [checkedInExceeded, participantsCheckedIn_number];
154}
155
156function calculateCrossSellingInitialPrice(requirements, participants, companions) {
157    let participantsIncluded = requirements.participantsIncluded;
158    let participantsCosts = requirements.participantsCosts;
159    let participantsPrice = calculateParticipantsPrice(participants, participantsIncluded, participantsCosts);
160
161    var companionsPrice = 0;
162    if (requirements.companionsEnabled) {
163        let companionsIncluded = requirements.companionsIncluded;
164        let companionsCosts = requirements.companionsCosts;
165        companionsPrice = calculateParticipantsPrice(companions, companionsIncluded, companionsCosts);
166    }
167
168    return requirements.basePrice + participantsPrice + companionsPrice;
169}
170
171function calculateParticipantsPrice(participants, participantsIncluded, participantsCosts) {
172    let costingParticipants = (participants - participantsIncluded) > 0 ? (participants - participantsIncluded) : 0;
173    let participantsPrice = participantsCosts * costingParticipants;
174    
175    return participantsPrice;
176}
177
178/** ### CROSS-SELLING ###
179 * 
180 * @typedef {object} CrossSellingConfiguration
181 * @property {object} requirements
182 * @property {CrossSelling_Group[][]} tabs
183 * @property {CrossSellingPage[]} pageInfos
184 * 
185 * 
186 * @typedef {object} CrossSellingPage
187 * @property {number} page_number
188 * @property {string} page_name
189 * @property {string} icon_style
190 * @property {string} icon_name
191 * 
192 * 
193 * @typedef {object} CrossSelling_Group
194 * @property {number} id
195 * @property {string} name
196 * @property {string} description
197 * @property {string} quantity_type
198 * @property {string} recommended_per_participant
199 * @property {string} select_type
200 * @property {boolean} required
201 * @property {CrossSelling_Item[]} articles
202 * 
203 * 
204 * @typedef {object} CrossSelling_Item
205 * @property {number} id
206 * @property {string} name
207 * @property {string} short_text
208 * @property {number} price_raw
209 * @property {string} price
210 * @property {string} image_path
211 * @property {number|null} articles_max
212*/
213
214/** ### CROSS-SELLING-SELECTION ###
215 * 
216 * @typedef {object} CrossSellingSelection
217 * @property {CrossSellingSelection_Group[]} itemGroups
218 * @property {object} participants
219 * @property {object} requirementsOld
220 * @property {number} initialPrice
221 * @property {number} totalPrice
222 * 
223 * 
224 * @typedef {object} CrossSellingSelection_Group
225 * @property {number} id
226 * @property {string} htmlID
227 * @property {string} groupName
228 * @property {number} groupPrice
229 * @property {CrossSellingSelection_Item[]} items
230 * 
231 * 
232 * @typedef {object} CrossSellingSelection_Item
233 * @property {number} id
234 * @property {string} name
235 * @property {number} price
236 * @property {number} quantity
237 * @property {string} quantityType
238*/
239
240var modalConfigPreset = {
241    itemGroups: [
242        {
243            id: -1,
244            htmlID: '',
245            groupName: '',
246            groupPrice: 0,
247            items: [
248                {
249                    id: -1,
250                    name: '',
251                    price: 0,
252                    quantity: 0,
253                    quantityType: '',
254                },
255            ],
256        },
257    ],
258    participants: {
259        main: [
260            {
261                name: '',
262                birthday: null,
263                age: null,
264            }
265        ],
266        normalQuantity: 0,
267        normal: [
268            {
269                name: '',
270            }
271        ],
272        companionsQuantity: 0,
273        companions: [
274            {
275                name: '',
276            }
277        ],
278    },
279    requirementsOld: {},
280    initialPrice: 0,
281    totalPrice: 0,
282}
283
284
285function updateCrossSellingConfig(csInstanceIndex) {
286    // reset and prepare modal config
287    var csConfig = JSON.parse(JSON.stringify(modalConfigPreset));
288    csConfig.itemGroups[1] = JSON.parse(JSON.stringify(csConfig.itemGroups[0]));
289    var useOldPricesWhenPossible = crossSellingInstances[csInstanceIndex].useOldPricesWhenPossible;
290    var previousSelection = crossSellingInstances[csInstanceIndex].crossSellingSelection;
291
292    // update html summary and modal config
293    var preparedCrossSelling = crossSellingInstances[csInstanceIndex].preparedCrossSelling;
294    var crossSellingContainer = crossSellingInstances[csInstanceIndex].insertContainer;
295    if (crossSellingContainer) {
296        var allArticleGroups = crossSellingContainer.querySelectorAll('.cross-selling-group');
297        if (allArticleGroups) {
298            // update initial price in summary
299            var participants = crossSellingInstances[csInstanceIndex].getParticipantsNumber();
300            var companions = crossSellingInstances[csInstanceIndex].getCompanionsNumber();
301            
302            var requirements = crossSellingInstances[csInstanceIndex].requirements;
303            if (useOldPricesWhenPossible && previousSelection && previousSelection.requirementsOld) {
304                let requirementsOld = previousSelection.requirementsOld;
305                
306                requirements.participantsIncluded = requirementsOld.participantsIncluded !== undefined ? requirementsOld.participantsIncluded : requirements.participantsIncluded;
307                requirements.participantsCosts = requirementsOld.participantsCosts !== undefined ? requirementsOld.participantsCosts : requirements.participantsCosts;
308                
309                requirements.companionsEnabled = requirementsOld.companionsEnabled !== undefined ? requirementsOld.companionsEnabled : requirements.companionsEnabled;
310                requirements.participantsMaxIncludeCompanions = requirementsOld.participantsMaxIncludeCompanions !== undefined ? requirementsOld.participantsMaxIncludeCompanions : requirements.participantsMaxIncludeCompanions;
311                
312                requirements.companionsIncluded = requirementsOld.companionsIncluded !== undefined ? requirementsOld.companionsIncluded : requirements.companionsIncluded;
313                requirements.companionsCosts = requirementsOld.companionsCosts !== undefined ? requirementsOld.companionsCosts : requirements.companionsCosts;
314                
315                requirements.basePrice = requirementsOld.basePrice !== undefined ? requirementsOld.basePrice : requirements.basePrice;
316                DEBUG && console.log('used old requirements for prices', requirementsOld);
317            }
318            
319            csConfig.requirementsOld = {
320                participantsIncluded: requirements.participantsIncluded,
321                participantsCosts: requirements.participantsCosts,
322                
323                companionsEnabled: requirements.companionsEnabled,
324                participantsMaxIncludeCompanions: requirements.participantsMaxIncludeCompanions,
325                
326                companionsIncluded: requirements.companionsIncluded,
327                companionsCosts: requirements.companionsCosts,
328                
329                basePrice: requirements.basePrice,
330            };
331            
332            csConfig.initialPrice = crossSellingInstances[csInstanceIndex].preSetInitialPrice;
333            DEBUG && console.log('pre set initial price', csConfig.initialPrice);
334            if (!isNumeric(csConfig.initialPrice)) {
335                if (crossSellingInstances[csInstanceIndex].missingSelection) { // add number of missing main participants
336                    let missingParticipants = crossSellingInstances[csInstanceIndex].missingSelection.participants;
337                    if (missingParticipants.main) {
338                        participants += missingParticipants.main.length;
339                    }
340                }
341                csConfig.initialPrice = calculateCrossSellingInitialPrice(requirements, participants, companions);
342                DEBUG && console.log('calculated initial price', csConfig.initialPrice);
343            }
344            csConfig.totalPrice = csConfig.initialPrice;
345
346            // iterate article groups
347            for (var currentArticleGroup of allArticleGroups) {
348                var checkedInputs = currentArticleGroup.querySelectorAll('input.cs-article-input:checked');
349
350                // process groups with articles selected
351                if (checkedInputs.length > 0) {
352                    var csGroupID = parseInt(currentArticleGroup.getAttribute('cs-group-id'));
353                    var articleGroupElementID = currentArticleGroup.getAttribute('id');
354
355                    // find the current group in the prepared CS to prevent manipulation in HTML
356                    articleIdentifiers = checkedInputs[0].getAttribute('prepared-cs-article-identifier').split('-');
357                    let tabIndex = parseInt(articleIdentifiers[0]);
358                    let groupIndex = parseInt(articleIdentifiers[1]);
359                    let articleIndex = parseInt(articleIdentifiers[2]);
360                    let preparedGroup = preparedCrossSelling[tabIndex][groupIndex];
361
362                    var articleGroupTitle = preparedGroup.name;
363                    var articleGroupFullPrice = 0.00;
364
365                    // reset summary group items
366
367                    // iterate selected articles
368                    for (currentTileInput of checkedInputs) {
369                        var inputID = currentTileInput.getAttribute('id');
370                        var currentTileLabel = crossSellingContainer.querySelector('label[for=' + inputID + ']');
371                        var currentArticleDiv = currentTileLabel.closest('.cross-selling-article');
372                        var csItemID = parseInt(currentArticleDiv.getAttribute('cs-article-id'));
373
374                        // determine oldItemPrice from previousSelection if wanted (by searching the arrays by the id-attribute)
375                        let oldItemPrice = false;
376                        if (useOldPricesWhenPossible && previousSelection) {
377                            let prevGroup = previousSelection.itemGroups.find(group => group.id == csGroupID);
378                            if (prevGroup) {
379                                let prevArticle = prevGroup.items.find(item => item.id == csItemID);
380                                if (prevArticle) {
381                                    oldItemPrice = prevArticle.price;
382                                }
383                            }
384                        }
385
386                        // find the current item and group in the prepared CS to prevent manipulation in HTML
387                        articleIdentifiers = currentTileInput.getAttribute('prepared-cs-article-identifier').split('-');
388                        tabIndex = parseInt(articleIdentifiers[0]);
389                        groupIndex = parseInt(articleIdentifiers[1]);
390                        articleIndex = parseInt(articleIdentifiers[2]);
391                        let preparedGroup = preparedCrossSelling[tabIndex][groupIndex];
392                        let preparedArticle = preparedCrossSelling[tabIndex][groupIndex].articles[articleIndex];
393
394                        var tileTitle = preparedArticle.name;
395                        var quantityType = preparedGroup.quantity_type;
396                        var articlePrice = (oldItemPrice !== false) ? oldItemPrice : parseFloat(preparedArticle.price_raw);
397
398                        // set quantity and show relevant quantity type label
399                        var quantity = -1;
400                        switch (quantityType) {
401                            case 'over_all':
402                                quantity = 1;
403                                break;
404                            case 'per_participant':
405                                quantity = participants;
406                                break;
407                            case 'free':
408                                quantity = parseInt(currentTileLabel.querySelector('.item-count').value);
409                                break;
410                        }
411                        let fullItemPrice = articlePrice * quantity;
412
413                        articleGroupFullPrice += fullItemPrice;
414
415                        // set modal config item values
416                        let itemGroups = csConfig.itemGroups;
417                        let items = itemGroups[itemGroups.length - 1].items;
418                        let nextItemIndex = items.length;
419                        items[nextItemIndex] = JSON.parse(JSON.stringify(items[0]));
420                        items[nextItemIndex].id = csItemID;
421                        items[nextItemIndex].name = tileTitle;
422                        items[nextItemIndex].price = articlePrice;
423                        items[nextItemIndex].quantity = quantity;
424                        items[nextItemIndex].quantityType = quantityType;
425                    }
426
427                    // set modal config group values
428                    let itemGroups = csConfig.itemGroups;
429                    let nextGroupIndex = itemGroups.length;
430                    itemGroups[nextGroupIndex - 1].id = csGroupID;
431                    itemGroups[nextGroupIndex - 1].htmlID = articleGroupElementID;
432                    itemGroups[nextGroupIndex - 1].groupName = articleGroupTitle;
433                    itemGroups[nextGroupIndex - 1].groupPrice = articleGroupFullPrice;
434                    itemGroups[nextGroupIndex] = JSON.parse(JSON.stringify(itemGroups[0]));
435                    csConfig.totalPrice += articleGroupFullPrice;
436                }
437            }
438        }
439
440        // clean up modal config
441        let itemGroups = csConfig.itemGroups;
442        if (itemGroups[0].groupName == '') {
443            itemGroups.shift();
444        }
445        if (itemGroups[itemGroups.length - 1].groupName == '') {
446            itemGroups.pop();
447        }
448        for (currentGroup of itemGroups) {
449            if (currentGroup.items[0].name == '') {
450                currentGroup.items.shift();
451            }
452        }
453
454        // set modal config participants data
455        var configParticipants = csConfig.participants;
456        var config = [
457            [configParticipants.main, []],
458            [configParticipants.normal, []],
459        ]
460        if (requirements.companionsEnabled) {
461            config.push([configParticipants.companions, []]);
462        }
463
464        if (participants){
465            configParticipants.normalQuantity = participants;
466        }
467        if (companions && requirements.companionsEnabled){
468            configParticipants.companionsQuantity = companions;
469        }
470
471        if (crossSellingInstances[csInstanceIndex].getMainParticipantsList()) {
472            config[0].pop();
473            config[0].push(crossSellingInstances[csInstanceIndex].getAllMainParticipants());
474        }
475        if (crossSellingInstances[csInstanceIndex].getParticipantsList()) {
476            config[1].pop();
477            config[1].push(crossSellingInstances[csInstanceIndex].getAllParticipants());
478        }
479        if (crossSellingInstances[csInstanceIndex].getCompanionsList() && (config.length > 2)) {
480            config[2].pop();
481            config[2].push(crossSellingInstances[csInstanceIndex].getAllCompanions());
482        }
483
484        for (currentPair of config) {
485            currentParticipantsNames = currentPair[0];
486            htmlParticipants = currentPair[1];
487            if (htmlParticipants.length > 0) {
488                for (currentParticipant of htmlParticipants) {
489                    nextParticipantIndex = currentParticipantsNames.length;
490                    currentParticipantsNames[nextParticipantIndex] = JSON.parse(JSON.stringify(currentParticipantsNames[0]));
491                    nextParticipant = currentParticipantsNames[nextParticipantIndex];
492                    nextParticipant.name = currentParticipant.querySelector('.participant-name').value;
493                    nextParticipant = setBirthdayAndOrAge_atConfigParticipant_fromInput(nextParticipant, currentParticipant);
494                }
495            }
496
497            // clean up participants data
498            for (var i = 0; i < currentParticipantsNames.length; i++) {
499                if (currentParticipantsNames[i].name == '') {
500                    currentParticipantsNames.splice(i, 1);
501                    i--;
502                }
503            }
504        }
505
506        let removeCompanionsProperties = !requirements.companionsEnabled;
507
508        if (crossSellingInstances[csInstanceIndex].missingSelection) {
509            DEBUG && console.log('cross-selling missingSelection', crossSellingInstances[csInstanceIndex].missingSelection);
510            let missingGroups = crossSellingInstances[csInstanceIndex].missingSelection.itemGroups;
511            if (missingGroups.length) {
512                for (let missingGroup of missingGroups) {
513                    let groupID = missingGroup.id;
514                    let existingGroup = csConfig.itemGroups.find(group => group.id == groupID);
515                    if (existingGroup) {
516                        existingGroup.items.push(...missingGroup.items); // add all missing items to existing group
517                        existingGroup.groupPrice += missingGroup.groupPrice; // add price of all items to existing group price
518                        csConfig.totalPrice += missingGroup.groupPrice; // add price of all items to total price
519                    } else {
520                        csConfig.itemGroups.push(missingGroup); // add missing group
521                        csConfig.totalPrice += missingGroup.groupPrice; // add price of missing group to total price
522                    }
523                }
524            }
525            let missingParticipants = crossSellingInstances[csInstanceIndex].missingSelection.participants;
526            if (missingParticipants.main) {
527                csConfig.participants.main.push(...missingParticipants.main);
528            }
529            if (missingParticipants.normal) {
530                csConfig.participants.normal.push(...missingParticipants.normal);
531            }
532            if (missingParticipants.companions) {
533                csConfig.participants.companions.push(...missingParticipants.companions);
534                removeCompanionsProperties = false;
535            }
536        }
537
538        if (removeCompanionsProperties) {
539            delete configParticipants.companions;
540            delete configParticipants.companionsQuantity;
541        }
542
543        DEBUG && console.log('cross-selling config', csConfig);
544        crossSellingInstances[csInstanceIndex].crossSellingConfig = csConfig;
545    }
546}
547
548function setBirthdayAndOrAge_atConfigParticipant_fromInput(configParticipant, participantInput) {
549    let birthdayInput = participantInput.querySelector('.participant-birthday-age.birthday');
550    if (birthdayInput) {
551        configParticipant.birthday = birthdayInput.value;
552    }
553    let ageInput = participantInput.querySelector('.participant-birthday-age.age');
554    if (ageInput) {
555        configParticipant.age = parseInt(ageInput.value);
556    }
557
558    return configParticipant;
559}
560
561function setBirthdayAndOrAge_atInputs_fromConfigParticipant(participantElement, configParticipant) {
562    let birthdayInput = participantElement.querySelector('input.participant-birthday-age.birthday');
563    if (birthdayInput && configParticipant_hasBirthday(configParticipant)) {
564        birthdayInput.value = configParticipant.birthday;
565    }
566    let ageInput = participantElement.querySelector('input.participant-birthday-age.age');
567    if (ageInput && configParticipant_hasAge(configParticipant)) {
568        ageInput.value = configParticipant.age;
569    }
570}
571
572function configParticipant_hasBirthday(configParticipant) {
573    return (configParticipant.hasOwnProperty('birthday') && configParticipant.birthday);
574}
575
576function configParticipant_hasAge(configParticipant) {
577    return (configParticipant.hasOwnProperty('age') && isNumeric(configParticipant.age));
578}
579
580//###############################################################################################################################################
581
582function getCrossSellingHtml(crossSellingData, csInstanceIndex = false) {
583    DEBUG && console.log('getCrossSellingHtml() called');
584    var params = {
585        data: crossSellingData,
586        csInstanceIndex: csInstanceIndex 
587    };
588    return new Promise(
589        function(resolve, reject) {
590            let xhttp = new XMLHttpRequest();
591            xhttp.onload = function() {
592                resolve(this.responseText);
593            }
594            xhttp.open("POST", "/articles/cross-selling-groups/getcrosssellinghtml" + new QuerySearchParams(params), true);
595            xhttp.setRequestHeader('Content-Type', 'application/json');
596            xhttp.send(JSON.stringify(params));
597        }
598    );
599}
600
601function getCrossSellingHtmlTable(crossSellingData, csInstanceIndex = false) {
602    DEBUG && console.log('getCrossSellingHtmlTable() called');
603    var params = {
604        data: crossSellingData,
605        csInstanceIndex: csInstanceIndex 
606    };
607    return new Promise(
608        function(resolve, reject) {
609            let xhttp = new XMLHttpRequest();
610            xhttp.onload = function() {
611                resolve(this.responseText);
612            }
613            xhttp.open("POST", "/articles/cross-selling-groups/getcrosssellinghtmltable" + new QuerySearchParams(params), true);
614            xhttp.setRequestHeader('Content-Type', 'application/json');
615            xhttp.send(JSON.stringify(params));
616        }
617    );
618}
619
620//###############################################################################################################################################
621
622
623class ArticleEdit_Abstract {
624    /** @type {HTMLTableRowElement} */
625    #row;
626    get row() {
627        return this.#row;
628    }
629    
630    get quantityMin() {
631        return isNumeric(this._quantityMin) ? parseInt(this._quantityMin) : -Infinity;
632    }
633    get quantityMax() {
634        return isNumeric(this._quantityMax) ? parseInt(this._quantityMax) : Infinity;
635    }
636
637    
638    get ordersHasArticleID() { // ! OVERRIDE IN SUBCLASS
639        throw new TypeError('Must override `this.ordersHasArticleID` getter!');
640    }
641    
642    get articleName() { // ! OVERRIDE IN SUBCLASS
643        throw new TypeError('Must override `this.articleName` getter!');
644    }
645    
646    get currentQuantity() { // ! OVERRIDE IN SUBCLASS
647        throw new TypeError('Must override `this.currentQuantity` getter!');
648    }
649    set currentQuantity(quantity) { // ! OVERRIDE IN SUBCLASS
650        throw new TypeError('Must override `this.currentQuantity` setter!');
651    }
652    
653    get newQuantity() { // ! OVERRIDE IN SUBCLASS
654        throw new TypeError('Must override `this.newQuantity` getter!');
655    }
656    set newQuantity(quantity) { // ! OVERRIDE IN SUBCLASS
657        throw new TypeError('Must override `this.newQuantity` setter!');
658    }
659    
660    get _quantityMin() { // ! OVERRIDE IN SUBCLASS
661        throw new TypeError('Must override `this._quantityMin` getter!');
662    }
663    get _quantityMax() { // ! OVERRIDE IN SUBCLASS
664        throw new TypeError('Must override `this._quantityMax` getter!');
665    }
666
667    /**
668     * @param {HTMLTableRowElement} row
669     */
670    constructor(row) {
671        if (new.target === ArticleEdit_Abstract) {
672            throw new TypeError('Cannot instantiate Abstract class!');
673        }
674        
675        this.#row = row;
676        
677        // ? INFO: cannot reliably check getters/setters in constructor
678        
679        if (typeof this.start != 'function') { // ! OVERRIDE IN SUBCLASS
680            throw new TypeError('Must override `this.start` method!');
681        }
682        if (typeof this.close != 'function') { // ! OVERRIDE IN SUBCLASS
683            throw new TypeError('Must override `this.close` method!');
684        }
685        
686        if (typeof this.block != 'function') { // ! OVERRIDE IN SUBCLASS
687            throw new TypeError('Must override `this.block` method!');
688        }
689        if (typeof this.unblock != 'function') { // ! OVERRIDE IN SUBCLASS
690            throw new TypeError('Must override `this.unblock` method!');
691        }
692        
693        if (typeof this.updateArticleRow != 'function') { // ! OVERRIDE IN SUBCLASS
694            throw new TypeError('Must override `this.updateArticleRow` method!');
695        }
696    }
697
698    async submit() {
699        let oldArticleCount = this.currentQuantity;
700        let newArticleCount = this.newQuantity;
701        
702        if (oldArticleCount == newArticleCount) {
703            this.close();
704            return;
705        }
706        
707        this.block();
708        
709        let ajaxResult = await Ajax.put('/articles/orders-has-articles/set-quantity', {
710            ordersHasArticleID: this.ordersHasArticleID,
711            quantity: newArticleCount,
712        });
713        
714        if (ajaxResult.requestWasSuccessful == false) {
715            this.sendErrorMessage(ajaxResult.errorMessage);
716            this.start();
717            this.unblock();
718            return;
719        }
720        if (ajaxResult.resultJSON.error == true) {
721            this.sendErrorMessage(ajaxResult.resultJSON.message);
722            this.start();
723            this.unblock();
724            return;
725        }
726        
727        let orderArticle = ajaxResult.resultJSON.returnData.ordersHasArticle;
728        
729        this.sendSuccessMessage();
730        this.updateArticleRow(orderArticle);
731        this.close();
732        this.unblock();
733    }
734    
735    /**
736     * @param {string} message
737     */
738    sendSuccessMessage(message = '') {
739        message = (typeof message == 'string' && message.length > 0) ? `: ${message}` : '';
740        notification(jsTranslations.ARTICLE_EDIT_SUCCESS_MESSAGE.replace('%ARTICLE_NAME%', this.articleName) + message, 'success');
741    }
742    /**
743     * @param {string} message
744     */
745    sendErrorMessage(message) {
746        notification(jsTranslations.ARTICLE_EDIT_ERROR_MESSAGE.replace('%ARTICLE_NAME%', this.articleName) + `: ${message}`, 'error');
747    }
748}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.