diff --git a/assets/js/form.js b/assets/js/form.js new file mode 100644 index 0000000..a0300eb --- /dev/null +++ b/assets/js/form.js @@ -0,0 +1,350 @@ +/** + * Umzugsliste Form JavaScript + * + * Real-time volume (cbm) calculations matching legacy logic + * + * @package Umzugsliste + */ + +(function($) { + 'use strict'; + + /** + * Parse German decimal format to float + * Converts "0,40" or "0.40" to 0.40 + * + * @param {string|number} str Value to parse + * @return {number} Parsed number or 0 + */ + function parseGermanDecimal(str) { + if (!str || str === '') { + return 0; + } + + // Convert to string and trim + str = String(str).trim().replace(',', '.'); + + // Parse as float + const num = parseFloat(str); + + // Return 0 for invalid or negative numbers + return isNaN(num) || num < 0 ? 0 : num; + } + + /** + * Format number to German decimal format + * Converts 0.40 to "0,40" + * + * @param {number} num Number to format + * @param {number} decimals Number of decimal places (default 2) + * @return {string} Formatted number string + */ + function formatGermanDecimal(num, decimals) { + decimals = decimals || 2; + return num.toFixed(decimals).replace('.', ','); + } + + /** + * Calculate total cbm for a single furniture item + * + * @param {string|number} quantity Item quantity + * @param {string|number} cbm CBM value per item + * @return {number} Total cbm for this item + */ + function calculateItemTotal(quantity, cbm) { + const qty = parseGermanDecimal(quantity); + const cbmVal = parseGermanDecimal(cbm); + return qty * cbmVal; + } + + /** + * Calculate totals for a single room + * + * @param {string} roomKey Room identifier (e.g., "wohnzimmer") + * @return {object} Object with quantity and cbm totals + */ + function calculateRoomTotal(roomKey) { + let totalCbm = 0; + let totalQuantity = 0; + + // Find all furniture rows for this room + $('tr[data-room="' + roomKey + '"].furniture-row').each(function() { + const $row = $(this); + const quantity = $row.find('.quantity-input').val(); + const cbm = $row.data('cbm'); + + const qty = parseGermanDecimal(quantity); + totalQuantity += qty; + totalCbm += calculateItemTotal(quantity, cbm); + }); + + // Round to 2 decimal places + return { + quantity: totalQuantity, + cbm: Math.round(totalCbm * 100) / 100 + }; + } + + /** + * Calculate grand totals across all rooms + * + * @return {object} Object with quantity and cbm totals + */ + function calculateGrandTotal() { + let totalCbm = 0; + let totalQuantity = 0; + + // Sum all room totals + $('.room-totals').each(function() { + const $row = $(this); + const roomKey = $row.closest('table').data('room'); + const roomTotal = calculateRoomTotal(roomKey); + + totalQuantity += roomTotal.quantity; + totalCbm += roomTotal.cbm; + }); + + // Round to 2 decimal places + return { + quantity: totalQuantity, + cbm: Math.round(totalCbm * 100) / 100 + }; + } + + /** + * Update display for a single room's totals + * + * @param {string} roomKey Room identifier + */ + function updateRoomDisplay(roomKey) { + const total = calculateRoomTotal(roomKey); + const $table = $('table[data-room="' + roomKey + '"]'); + + $table.find('.room-total-quantity').text(total.quantity); + $table.find('.room-total-cbm').text(formatGermanDecimal(total.cbm)); + } + + /** + * Update grand totals display + */ + function updateGrandTotalDisplay() { + const total = calculateGrandTotal(); + + $('#grand-total-quantity').text(total.quantity); + $('#grand-total-cbm').text(formatGermanDecimal(total.cbm)); + } + + /** + * Update all totals (rooms and grand total) + */ + function updateAllTotals() { + // Update each room + $('.room-totals').each(function() { + const roomKey = $(this).closest('table').data('room'); + updateRoomDisplay(roomKey); + }); + + // Update grand total + updateGrandTotalDisplay(); + } + + /** + * Handle quantity input change + * Debounced for performance + */ + let debounceTimer; + function handleQuantityChange() { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(function() { + updateAllTotals(); + }, 100); // Quick response (100ms debounce) + } + + /** + * Validate email format + * + * @param {string} email Email address to validate + * @return {boolean} True if valid email format + */ + function validateEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); + } + + /** + * Validate required field + * + * @param {string} value Field value + * @return {boolean} True if not empty + */ + function validateRequired(value) { + return value && value.trim() !== ''; + } + + /** + * Show error message for a field + * + * @param {jQuery} $field Field element + * @param {string} message Error message + */ + function showFieldError($field, message) { + // Add error class to field + $field.addClass('field-error'); + + // Remove existing error message if any + clearFieldError($field); + + // Add error message after field + $field.after('' + message + ''); + } + + /** + * Clear error message for a field + * + * @param {jQuery} $field Field element + */ + function clearFieldError($field) { + $field.removeClass('field-error'); + $field.next('.error-message').remove(); + } + + /** + * Validate a single field + * + * @param {jQuery} $field Field element + * @return {boolean} True if valid + */ + function validateField($field) { + const fieldName = $field.attr('name'); + const value = $field.val(); + const isRequired = $field.attr('required') !== undefined; + + // Clear existing errors + clearFieldError($field); + + // Check required fields + if (isRequired && !validateRequired(value)) { + showFieldError($field, 'Dieses Feld ist erforderlich'); + return false; + } + + // Check email format + if (fieldName === 'info[eE-Mail]' && value) { + if (!validateEmail(value)) { + showFieldError($field, 'Bitte geben Sie eine gültige E-Mail-Adresse ein'); + return false; + } + } + + return true; + } + + /** + * Validate all furniture items - at least one must have quantity + * + * @return {boolean} True if valid + */ + function validateFurnitureItems() { + let hasItems = false; + + $('.quantity-input').each(function() { + const qty = parseGermanDecimal($(this).val()); + if (qty > 0) { + hasItems = true; + return false; // break loop + } + }); + + return hasItems; + } + + /** + * Validate date fields + * + * @return {boolean} True if valid date selected + */ + function validateDate() { + const day = $('select[name="day"]').val(); + const month = $('select[name="month"]').val(); + const year = $('select[name="year"]').val(); + + return day && month && year; + } + + /** + * Validate entire form before submission + * + * @return {boolean} True if all validations pass + */ + function validateForm() { + let isValid = true; + const errors = []; + + // Validate date + if (!validateDate()) { + errors.push('Bitte wählen Sie ein vollständiges Umzugsdatum'); + isValid = false; + } + + // Validate required fields + $('input[required]').each(function() { + if (!validateField($(this))) { + isValid = false; + } + }); + + // Validate furniture items + if (!validateFurnitureItems()) { + errors.push('Bitte geben Sie mindestens ein Möbelstück ein'); + isValid = false; + + // Scroll to first room table + if ($('.quantity-input:first').length) { + $('html, body').animate({ + scrollTop: $('.quantity-input:first').closest('table').offset().top - 100 + }, 500); + } + } + + // If there are general errors, scroll to first error field + if (!isValid && $('.field-error:first').length) { + $('html, body').animate({ + scrollTop: $('.field-error:first').offset().top - 100 + }, 500); + } + + return isValid; + } + + /** + * Initialize calculations + */ + $(document).ready(function() { + // Attach event listeners to all quantity inputs + $('.quantity-input').on('input change', handleQuantityChange); + + // Initial calculation (in case of pre-filled values) + updateAllTotals(); + + // Attach validation listeners + $('input[required], input[type="email"]').on('blur', function() { + validateField($(this)); + }); + + // Clear error on field change + $('input').on('input', function() { + clearFieldError($(this)); + }); + + // Validate form on submit + $('#umzugsliste-form').on('submit', function(e) { + if (!validateForm()) { + e.preventDefault(); + return false; + } + }); + + console.log('Umzugsliste calculations and validation initialized'); + }); + +})(jQuery);