chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
482
care-booking-block/public/js/frontend.js
Normal file
482
care-booking-block/public/js/frontend.js
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Care Booking Block - Frontend JavaScript
|
||||
*
|
||||
* Provides graceful degradation and enhanced interaction for KiviCare integration
|
||||
*
|
||||
* @package CareBookingBlock
|
||||
*/
|
||||
|
||||
(function($, config) {
|
||||
'use strict';
|
||||
|
||||
// Global configuration
|
||||
const CareBooking = {
|
||||
config: config || {},
|
||||
initialized: false,
|
||||
retryCount: 0,
|
||||
observers: [],
|
||||
|
||||
/**
|
||||
* Initialize the Care Booking frontend functionality
|
||||
*/
|
||||
init: function() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('Care Booking Block: Initializing frontend scripts');
|
||||
}
|
||||
|
||||
this.setupObservers();
|
||||
this.enhanceExistingElements();
|
||||
this.setupEventListeners();
|
||||
this.setupFallbacks();
|
||||
|
||||
this.initialized = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup MutationObserver to watch for dynamically added content
|
||||
*/
|
||||
setupObservers: function() {
|
||||
if (!window.MutationObserver) {
|
||||
if (this.config.debug) {
|
||||
console.warn('Care Booking Block: MutationObserver not supported');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let hasNewContent = false;
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if new node contains KiviCare content
|
||||
if (this.hasKiviCareContent(node)) {
|
||||
hasNewContent = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (hasNewContent) {
|
||||
this.enhanceNewContent();
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
this.observers.push(observer);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if element contains KiviCare content
|
||||
* @param {Element} element
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasKiviCareContent: function(element) {
|
||||
const selectors = [
|
||||
this.config.selectors.doctors,
|
||||
this.config.selectors.services,
|
||||
this.config.selectors.forms
|
||||
].join(', ');
|
||||
|
||||
return $(element).find(selectors).length > 0 || $(element).is(selectors);
|
||||
},
|
||||
|
||||
/**
|
||||
* Enhance existing KiviCare elements on page load
|
||||
*/
|
||||
enhanceExistingElements: function() {
|
||||
this.enhanceLoadingStates();
|
||||
this.enhanceFormValidation();
|
||||
this.enhanceFallbackElements();
|
||||
},
|
||||
|
||||
/**
|
||||
* Enhance newly added content
|
||||
*/
|
||||
enhanceNewContent: function() {
|
||||
if (this.config.debug) {
|
||||
console.log('Care Booking Block: Enhancing new content');
|
||||
}
|
||||
|
||||
// Add a small delay to ensure DOM is stable
|
||||
setTimeout(() => {
|
||||
this.enhanceExistingElements();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup loading states for better UX
|
||||
*/
|
||||
enhanceLoadingStates: function() {
|
||||
const $forms = $(this.config.selectors.forms);
|
||||
|
||||
$forms.each((index, form) => {
|
||||
const $form = $(form);
|
||||
|
||||
// Add loading indicator
|
||||
if (!$form.find('.care-booking-loading').length) {
|
||||
$form.prepend('<div class="care-booking-loading" style="display: none;">Loading...</div>');
|
||||
}
|
||||
|
||||
// Handle form submissions
|
||||
$form.on('submit', (e) => {
|
||||
this.showLoadingState($form);
|
||||
});
|
||||
|
||||
// Handle AJAX requests
|
||||
$(document).on('ajaxStart', () => {
|
||||
if (this.isKiviCareAjax()) {
|
||||
this.showLoadingState($form);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('ajaxComplete', () => {
|
||||
this.hideLoadingState($form);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show loading state
|
||||
* @param {jQuery} $element
|
||||
*/
|
||||
showLoadingState: function($element) {
|
||||
$element.addClass('care-booking-loading');
|
||||
$element.find('.care-booking-loading').show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide loading state
|
||||
* @param {jQuery} $element
|
||||
*/
|
||||
hideLoadingState: function($element) {
|
||||
$element.removeClass('care-booking-loading');
|
||||
$element.find('.care-booking-loading').hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if current AJAX request is KiviCare related
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isKiviCareAjax: function() {
|
||||
// This is a simplified check - could be enhanced based on KiviCare's AJAX patterns
|
||||
return window.location.href.indexOf('kivicare') !== -1 ||
|
||||
document.body.className.indexOf('kivicare') !== -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enhance form validation
|
||||
*/
|
||||
enhanceFormValidation: function() {
|
||||
const $forms = $(this.config.selectors.forms);
|
||||
|
||||
$forms.each((index, form) => {
|
||||
const $form = $(form);
|
||||
|
||||
$form.on('submit', (e) => {
|
||||
if (!this.validateBookingForm($form)) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time validation for select fields
|
||||
$form.find('select').on('change', (e) => {
|
||||
this.validateSelectField($(e.target));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate booking form
|
||||
* @param {jQuery} $form
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validateBookingForm: function($form) {
|
||||
let isValid = true;
|
||||
const requiredFields = $form.find('select[required], input[required]');
|
||||
|
||||
requiredFields.each((index, field) => {
|
||||
const $field = $(field);
|
||||
if (!$field.val() || $field.val() === '0' || $field.val() === '') {
|
||||
isValid = false;
|
||||
this.showFieldError($field, 'This field is required');
|
||||
} else {
|
||||
this.clearFieldError($field);
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate individual select field
|
||||
* @param {jQuery} $field
|
||||
*/
|
||||
validateSelectField: function($field) {
|
||||
const value = $field.val();
|
||||
|
||||
if ($field.attr('required') && (!value || value === '0' || value === '')) {
|
||||
this.showFieldError($field, 'Please make a selection');
|
||||
} else {
|
||||
this.clearFieldError($field);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show field error
|
||||
* @param {jQuery} $field
|
||||
* @param {string} message
|
||||
*/
|
||||
showFieldError: function($field, message) {
|
||||
$field.addClass('error');
|
||||
|
||||
let $error = $field.siblings('.field-error');
|
||||
if (!$error.length) {
|
||||
$error = $('<div class="field-error"></div>');
|
||||
$field.after($error);
|
||||
}
|
||||
|
||||
$error.text(message).show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear field error
|
||||
* @param {jQuery} $field
|
||||
*/
|
||||
clearFieldError: function($field) {
|
||||
$field.removeClass('error');
|
||||
$field.siblings('.field-error').hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup fallback elements for graceful degradation
|
||||
*/
|
||||
enhanceFallbackElements: function() {
|
||||
// Add fallback classes to elements that might be blocked
|
||||
$(this.config.selectors.doctors).each((index, element) => {
|
||||
const $element = $(element);
|
||||
if (!$element.hasClass('care-booking-fallback')) {
|
||||
$element.addClass('care-booking-enhanced');
|
||||
}
|
||||
});
|
||||
|
||||
$(this.config.selectors.services).each((index, element) => {
|
||||
const $element = $(element);
|
||||
if (!$element.hasClass('care-booking-fallback')) {
|
||||
$element.addClass('care-booking-enhanced');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup event listeners
|
||||
*/
|
||||
setupEventListeners: function() {
|
||||
// Handle dynamic doctor selection
|
||||
$(document).on('change', 'select[name="doctor_id"], .doctor-selection', (e) => {
|
||||
this.handleDoctorChange($(e.target));
|
||||
});
|
||||
|
||||
// Handle service selection
|
||||
$(document).on('change', 'select[name="service_id"], .service-selection', (e) => {
|
||||
this.handleServiceChange($(e.target));
|
||||
});
|
||||
|
||||
// Handle retry buttons
|
||||
$(document).on('click', '.care-booking-retry', (e) => {
|
||||
e.preventDefault();
|
||||
this.retryOperation($(e.target));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle doctor selection change
|
||||
* @param {jQuery} $select
|
||||
*/
|
||||
handleDoctorChange: function($select) {
|
||||
const doctorId = $select.val();
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('Care Booking Block: Doctor changed to', doctorId);
|
||||
}
|
||||
|
||||
// Clear service selection if doctor changed
|
||||
const $serviceSelect = $select.closest('form').find('select[name="service_id"], .service-selection');
|
||||
if ($serviceSelect.length) {
|
||||
$serviceSelect.val('').trigger('change');
|
||||
this.updateServiceOptions($serviceSelect, doctorId);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle service selection change
|
||||
* @param {jQuery} $select
|
||||
*/
|
||||
handleServiceChange: function($select) {
|
||||
const serviceId = $select.val();
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('Care Booking Block: Service changed to', serviceId);
|
||||
}
|
||||
|
||||
// Additional service-specific logic can be added here
|
||||
},
|
||||
|
||||
/**
|
||||
* Update service options based on selected doctor
|
||||
* @param {jQuery} $serviceSelect
|
||||
* @param {string} doctorId
|
||||
*/
|
||||
updateServiceOptions: function($serviceSelect, doctorId) {
|
||||
if (!doctorId || doctorId === '0') {
|
||||
return;
|
||||
}
|
||||
|
||||
// This would typically make an AJAX request to get services
|
||||
// For now, we'll rely on KiviCare's existing functionality
|
||||
$serviceSelect.trigger('doctor_changed', [doctorId]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup fallback mechanisms
|
||||
*/
|
||||
setupFallbacks: function() {
|
||||
if (!this.config.fallbackEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup automatic retry for failed operations
|
||||
this.setupAutoRetry();
|
||||
|
||||
// Setup offline detection
|
||||
this.setupOfflineDetection();
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup automatic retry for failed operations
|
||||
*/
|
||||
setupAutoRetry: function() {
|
||||
$(document).on('ajaxError', (event, jqXHR, ajaxSettings, thrownError) => {
|
||||
if (this.isKiviCareAjax() && this.retryCount < this.config.retryAttempts) {
|
||||
setTimeout(() => {
|
||||
this.retryCount++;
|
||||
if (this.config.debug) {
|
||||
console.log('Care Booking Block: Retrying operation, attempt', this.retryCount);
|
||||
}
|
||||
|
||||
// Retry the failed request
|
||||
$.ajax(ajaxSettings);
|
||||
}, this.config.retryDelay);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup offline detection
|
||||
*/
|
||||
setupOfflineDetection: function() {
|
||||
$(window).on('online offline', (e) => {
|
||||
const isOnline = e.type === 'online';
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('Care Booking Block: Connection status changed to', isOnline ? 'online' : 'offline');
|
||||
}
|
||||
|
||||
if (isOnline) {
|
||||
// Retry any pending operations
|
||||
this.retryPendingOperations();
|
||||
} else {
|
||||
// Show offline message
|
||||
this.showOfflineMessage();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retry pending operations when back online
|
||||
*/
|
||||
retryPendingOperations: function() {
|
||||
// Implementation would depend on what operations need to be retried
|
||||
if (this.config.debug) {
|
||||
console.log('Care Booking Block: Retrying pending operations');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show offline message
|
||||
*/
|
||||
showOfflineMessage: function() {
|
||||
const message = '<div class="care-booking-offline-message">You appear to be offline. Some features may not work properly.</div>';
|
||||
|
||||
if (!$('.care-booking-offline-message').length) {
|
||||
$('body').prepend(message);
|
||||
|
||||
setTimeout(() => {
|
||||
$('.care-booking-offline-message').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Retry a specific operation
|
||||
* @param {jQuery} $button
|
||||
*/
|
||||
retryOperation: function($button) {
|
||||
const $container = $button.closest('.care-booking-container');
|
||||
this.showLoadingState($container);
|
||||
|
||||
// Simulate retry - in practice, this would repeat the failed operation
|
||||
setTimeout(() => {
|
||||
this.hideLoadingState($container);
|
||||
$button.closest('.error-message').fadeOut();
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
destroy: function() {
|
||||
// Remove observers
|
||||
this.observers.forEach(observer => observer.disconnect());
|
||||
this.observers = [];
|
||||
|
||||
// Remove event listeners
|
||||
$(document).off('.careBooking');
|
||||
|
||||
this.initialized = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
$(document).ready(() => {
|
||||
CareBooking.init();
|
||||
});
|
||||
|
||||
// Handle page unload
|
||||
$(window).on('beforeunload', () => {
|
||||
CareBooking.destroy();
|
||||
});
|
||||
|
||||
// Expose to global scope for debugging
|
||||
if (config && config.debug) {
|
||||
window.CareBooking = CareBooking;
|
||||
}
|
||||
|
||||
})(jQuery, window.careBookingConfig);
|
||||
Reference in New Issue
Block a user