]> _ Git - fluidbook-html5.git/commitdiff
wip #6864 @0.5
authorVincent Vanwaelscappel <vincent@cubedesigners.com>
Tue, 16 Apr 2024 09:23:36 +0000 (11:23 +0200)
committerVincent Vanwaelscappel <vincent@cubedesigners.com>
Tue, 16 Apr 2024 09:23:36 +0000 (11:23 +0200)
js/libs/fluidbook/cart/fluidbook.cart.bastide-resah.js [new file with mode: 0644]

diff --git a/js/libs/fluidbook/cart/fluidbook.cart.bastide-resah.js b/js/libs/fluidbook/cart/fluidbook.cart.bastide-resah.js
new file mode 100644 (file)
index 0000000..28b4bcc
--- /dev/null
@@ -0,0 +1,508 @@
+function FluidbookCartBastide(cart) {
+    this.cart = cart;
+    this.fluidbook = this.cart.fluidbook;
+    this.data = this.fluidbook.settings.basketReferences;
+    this.form_endpoint = this.fluidbook.service.getBaseURL()+'bastide'; // Where cart form is processed
+    this.init();
+}
+
+FluidbookCartBastide.prototype = {
+    init: function () {
+        let $this = this;
+        this.items = this.fluidbook.cache.get('cart', []);
+
+        // Save changes in quantity field
+        $(document).on('input', '#Bastide_cart [data-item-quantity]', function () {
+            let item_index = $(this).data('item-index');
+            let quantity = $(this).val();
+
+            if ($this.items[item_index]) {
+                $this.items[item_index].quantity = quantity;
+                $this.save();
+                $this.updateCart();
+            } else {
+                console.warn(`Unable to update quantity for cart item ${item_index}`);
+            }
+
+            return true;
+        });
+
+        // Handle "Valider ma sélection" button in cart
+        $(document).on(this.fluidbook.input.clickEvent, '#Bastide_cart [data-validate-cart]', function(event) {
+            event.preventDefault();
+
+            // Use the built-in HTML5 validation to make sure all fields are valid in the cart
+            let form = $(this).parents('form');
+            let isValid = form[0].reportValidity();
+
+            if (isValid) {
+                // Originally this was in the link's href but for some reason the validation was
+                // skipped and the URL change would always occur, even when using
+                // event.preventDefault() / event.stopImmediatePropagation / event.stopPropagation();
+                window.location.hash = '/cart/validate';
+            }
+        });
+
+        // Handle details form validation and submission
+        $(document).on(this.fluidbook.input.clickEvent, '#Bastide_user_details [data-send-cart]', function(event) {
+            event.preventDefault();
+
+            let form = $('#Bastide_user_details form:visible');
+
+            if (!form.length) {
+                console.warn('Error finding form...');
+                return false;
+            }
+
+            // Get the form name so that we can look up the field labels later
+            let form_name = form.data('name');
+            let forms = $this.getForms();
+
+            // Get the user's details from the form...
+            let user_data = new FormData(form[0]);
+            let user_details = {}; // Restructured user data to send to the endpoint
+
+            user_data.forEach(function(value, key) {
+                user_details[key] = {
+                    label: forms[form_name][key]['label'],
+                    value: value,
+                };
+            });
+
+            // Prepare the main form data for sending
+            let data = new FormData();
+
+            // To make our endpoint more flexible, we need to give it some extra details
+            data.append('action', 'process-cart'); // There could be other actions handled by the endpoint
+            data.append('fluidbook_id', $this.fluidbook.settings.id); // In case we need special treatment per Fluidbook
+
+            // Now add the user details to the main form data...
+            data.append('user_details', JSON.stringify(user_details));
+
+            // Get data from the cart summary that can be used to generate the XLS
+            // This is passed as a JSON string to make it easier to handle in the FormData
+            data.append('cart_items', JSON.stringify($this.getItemsForXLS()));
+            data.append('column_headings', JSON.stringify($this.getColumnsForXLS()));
+
+            // It's also possible that the Fluidbook will include a querystring parameter (?a=xxxxx) to determine who
+            // the e-mail should be sent to. This is needs to be passed to the endpoint for handling
+            let querystring = (new URL(document.location)).searchParams;
+            data.append('recipient_code', querystring.get('a')); // If the querystring isn't set, it will return null
+
+            // Next, make sure the form is valid. Uses the built-in HTML5 validation
+            if (form[0].reportValidity()) {
+                $.ajax({
+                    url: $this.form_endpoint,
+                    cache: false,
+                    data: data,
+                    processData: false,
+                    contentType: false,
+                    method: 'post',
+                    dataType: 'json',
+                    success: function (data) {
+
+                        if (!data.success) {
+                            console.warn('Error processing request', data);
+                        }
+
+                        let container = $('#Bastide_user_details');
+                        // Hide the form and footer content, then show the message from the endpoint
+                        container.find('.details-form, .details-footer').hide();
+                        container.find('.server-response').text(data.message).show();
+                    }
+                });
+            }
+
+            event.preventDefault();
+        });
+
+    },
+
+    // emptyCart: function () {
+    //     this.items = [];
+    //     this.updateCart();
+    //     this.save();
+    //     resize();
+    // },
+
+    getCartItemIndex: function(reference) {
+        let cartItems = this.getItems();
+        for (let i = 0; i < cartItems.length; i++) {
+            if (cartItems[i].reference === reference) {
+                return i;
+            }
+        }
+
+        return -1;
+    },
+
+    addToCart: function (reference) {
+        let existingIndex = this.getCartItemIndex(reference);
+
+        // If this item has already been added to the cart, increment the quantity instead of adding a new item
+        if (existingIndex >= 0) {
+            this.items[existingIndex].quantity += 1;
+        } else {
+            let item = {};
+            item.reference = reference;
+            item.quantity = 1; // default initial quantity
+
+            // Items are added to the beginning of the array so most recently added are displayed at the top
+            // This is better than reversing the array when we display it because we use the index for removing items
+            this.items.unshift(item);
+        }
+
+        this.save();
+        return true;
+    },
+
+    removeFromCart: function (index) {
+        if (index >= 0) {
+            this.items.splice(index, 1);
+            this.save();
+        }
+    },
+
+    save: function () {
+        this.fluidbook.cache.set('cart', this.getItems());
+        this.updateIcon();
+        //this.fluidbook.cart.updateLinks();
+    },
+
+    getItems: function () {
+        let res = [];
+        let $this = this;
+        $(this.items).each(function (index, item) {
+            if (typeof $this.data[item.reference] !== 'undefined') {
+                res.push(item);
+            }
+        });
+        return res;
+    },
+
+    // Required by FluidbookCart.updateLinks()
+    getItemsReferences: function () {
+        return this.getItems();
+    },
+
+    getItemCount: function () {
+        return this.getItems().length;
+    },
+
+    getItemsForXLS: function () {
+        let $this = this;
+        let items = [];
+
+        $.each(this.getItems(), function (index, cart_item) {
+
+            let item = {};
+            let cart_reference = cart_item['reference'];
+            let data = $this.data[cart_reference]; // Source data matched from spreadsheet
+
+            $.each($this.getColumnsForXLS(), function (key, title) {
+
+                switch(key) {
+                    case 'QUANTITY':
+                        item[key] = cart_item['quantity'];
+                        break;
+                    default:
+                        item[key] = data[key];
+                }
+            });
+
+            items.push(item);
+        });
+
+        return items;
+    },
+
+    updateCart: function () {
+        if ($('#Bastide_cart').length > 0) {
+            $('#Bastide_cart .content').html(this.getCartContent());
+            this.fluidbook.resize.resize();
+        }
+    },
+
+    updateIcon: function () {
+        $(this.fluidbook).trigger('fluidbook.cart.updateIcon', {number: this.getItemCount()});
+    },
+
+    openModal: function (title, content, callback) {
+        let view = `<div id="Bastide_cart">
+                        ${this.fluidbook.menu.getCaption(title)}
+                        <div class="content">
+                            ${content}
+                        </div>
+                    </div>`;
+        this.fluidbook.menu.viewWrap(view, 'cart');
+        callback();
+    },
+
+    openMenu: function (p1, p2, callback) {
+
+        this.fluidbook.menu.quickCloseView();
+
+        // Handle the validation screen when accessed via #/cart/validate
+        if (p1 === 'validate') {
+            return this.openCartUserDetails(callback);
+        }
+
+        // The cart opens every time an item is added, so the user can pick a colour and quantity
+        // These URLs use the #/cart/add/xxxxx URL to pass the reference across
+        if (p1 === 'add' && p2) {
+            this.addToCart(p2);
+        }
+        return this.openCart(p2, callback);
+    },
+
+    openCart: function (p2, callback) {
+        this.openModal('Ma Sélection', this.getCartContent(), function () {
+            callback();
+        });
+    },
+
+    openCartUserDetails: function (callback) {
+        $('html').removeClass('cart-form-visible'); // Make sure forms are hidden initially
+        this.openModal('Mes Coordonnées', this.getCartUserDetailsContent(), function () {
+            callback();
+        });
+    },
+
+    getColumns: function () {
+        // Map of data key names to their display labels - this controls the order of the columns
+        // Note: the key names here should match the first row column titles in the spreadsheet
+        // This can be overridden by specifying the cart_columns in the "Paramètres panier" field (cartExtraSettings)
+        // The format for the setting is:
+        // cart_columns=XLS COL NAME|Display label,XLS COL 2|Display label 2
+        if (fluidbook.settings.cartColumns) {
+            return fluidbook.settings.cartColumns;
+        }
+
+        return {
+            'ARTICLE CODE': 'Réf',
+            'ARTICLE': 'Article',
+            'CONDITIONNEMENT VENTE': 'Conditionnement',
+            'QUANTITY': 'Quantité', // Special case: not part of the data
+            'DELETE': '', // No column label for delete buttons
+        };
+    },
+
+    getColumnsForXLS: function () {
+        // The columns should be the same as for the cart except we skip "DELETE"
+        let columns = {};
+
+        $.each(this.getColumns(), function(key, title) {
+
+            if (key === 'DELETE') return;
+
+            columns[key] = title;
+        });
+
+        return columns;
+    },
+
+    // This cart was based off CFOC's, so we're keeping the structure where it's possible to have multiple forms
+    getForms: function () {
+        return {
+            'default': {
+                'name': {
+                    'label': "Nom et Prénom",
+                    'type': 'text',
+                    'required': true
+                },
+                'phone': {
+                    'label': "Téléphone",
+                    'type': 'text',
+                    'required': true
+                },
+                'email': {
+                    'label': "Email",
+                    'type': 'email',
+                    'required': true
+                },
+                'billing_address': {
+                    'label': "Adresse de facturation",
+                    'type': 'textarea',
+                    'rows': 2,
+                    'required': false
+                },
+                'message': {
+                    'label': "Message",
+                    'type': 'textarea',
+                    'rows': 3,
+                    'required': false
+                },
+            },
+        }
+    },
+
+    getCartContent: function () {
+        if (this.getItemCount() === 0) {
+            return `<div class="cart-empty">${ this.fluidbook.l10n.__('your cart is empty') }</div>`;
+        }
+
+        let $this = this;
+        let columns = this.getColumns();
+
+        let content =
+          `<div class="inner-content">
+             <form>
+             <table id="bastide-cart-table" class="cart-items">
+               <thead style="text-transform:uppercase">
+                 <tr>
+                   ${Object.entries(columns)
+                     .map(heading => `<th data-name="${heading[0]}">${heading[1]}</th>`)
+                     .join('')
+                   }
+                 </tr>
+               </thead>
+               <tbody>`;
+
+        $.each(this.getItems(), function (index, item) {
+
+            content += '<tr>';
+
+            $.each(columns, function (key, title) {
+
+                let data = $this.data[item.reference];
+                let value = data[key] || '&mdash;'; // Fallback for missing values
+                let output = '';
+
+                switch(key) {
+
+                    case 'PRIX':
+                        output += $this.formatPrice(value);
+                        break;
+                    case 'QUANTITY':
+                        let min_quantity = 1;
+
+                        output  = `<div class="quantity-controls">`;
+
+                        output += `<button onclick="this.parentNode.querySelector('input[type=number]').stepDown();
+                                                    this.parentNode.querySelector('input[type=number]').dispatchEvent(new Event('input', { bubbles: true }));
+                                                    return false;"
+                                           class="quantity-button">
+                                   <svg viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M64 224h384v64H64z"/></svg>
+                                   </button>`;
+
+                        output += `<input data-item-quantity type="number" min="${min_quantity}" value="${item.quantity}" step="${min_quantity}" data-item-index="${index}" required>`
+
+                        output += `<button onclick="this.parentNode.querySelector('input[type=number]').stepUp();
+                                                    this.parentNode.querySelector('input[type=number]').dispatchEvent(new Event('input', { bubbles: true }));
+                                                    return false;"
+                                           class="quantity-button">
+                                    <svg viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M448 224H288V64h-64v160H64v64h160v160h64V288h160z"/></svg>
+                                    </button>`;
+
+                        output += `</div>`; // .quantity-controls
+
+                        break;
+                    case 'DELETE':
+                        output = `<a href="#" data-cart-delete="${index}" title="Supprimer">
+                                    ${getSpriteIcon('interface-close')}
+                                 </a>`;
+                        break;
+                    default:
+                        output = value;
+                }
+
+                // The data-name is used as a hook for CSS styles
+                // The data-label is displayed as the inline label in small screen views
+                content += `<td data-name="${key}" data-label="${title}">${output}</td>`;
+            });
+
+            content += '</tr>';
+        });
+
+        content += '</tbody>';
+        content += '</table>';
+
+        content += `<div class="fonctions">
+                      <a href="#/closeview" class="close-cart">
+                        Compléter ma sélection
+                      </a>
+                      <a href="#" class="validate-cart" data-validate-cart>
+                        Valider ma Sélection
+                      </a>
+                    </div>`;
+
+        content += '</form>';
+        content += '</div><!-- .inner-content -->';
+
+        return content;
+    },
+
+    getCartUserDetailsContent: function() {
+
+        let forms = '';
+
+        $.each(this.getForms(), function(name, fields) {
+            forms += `<form data-name="${name}">`;
+
+            $.each(fields, function(field_name, field_details) {
+                let required = field_details.required ? 'required' : '';
+                let label = field_details.required ? `${field_details.label}*` : field_details.label;
+                let field_ID = `${name}_${field_name}`;
+
+                forms += `<label for="${field_ID}">${label}</label>`;
+
+                switch (field_details.type) {
+                    case 'textarea':
+                        forms += `<textarea id="${field_ID}" name="${field_name}" rows="${field_details.rows || 3}" ${required}></textarea>`
+                        break;
+                    default:
+                        forms += `<input id="${field_name}" name="${field_name}" type="${field_details.type}" ${required}>`;
+                }
+            });
+
+            forms += `</form>`;
+        });
+
+        let content =
+          `<div class="inner-content" id="Bastide_user_details">
+            <div class="details-form">
+              ${forms}
+              <div class="required-fields-notice">*Champs obligatoires</div>
+            </div><!-- .details-columns -->
+            <div class="details-footer fonctions">
+              <a href="#/cart" class="back-to-cart">
+                Retour à ma Sélection
+              </a>
+              <a href="#" class="send-cart" data-send-cart>Envoyer ma demande</a>
+            </div><!-- .details-footer -->
+            <div class="server-response" style="display:none"><!-- placeholder for AJAX response --></div>
+          </div><!-- .inner-content -->
+          `;
+
+        return content;
+    },
+
+    getMenuWidth: function () {
+        return window.location.hash === '#/cart/validate' ? 990 : 1190; // Validate modal is narrower than main cart
+    },
+
+    formatPrice: function (price) {
+
+        if (typeof price !== 'number') {
+            price = this.parseFloat(price);
+        }
+
+        return price.toLocaleString("fr-FR", {
+            style: "currency",
+            currency: "EUR",
+            minimumFractionDigits: 2,
+            maximumFractionDigits: 2
+        });
+    },
+
+    parseFloat: function (s) {
+        if (typeof s === 'number') {
+            return s;
+        }
+        if (s === undefined || s === null || s === '') {
+            return 0;
+        }
+        s = s.replace(/\s/g, '');
+        return parseFloat(s);
+    },
+};