- 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>
844 lines
25 KiB
JavaScript
844 lines
25 KiB
JavaScript
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
/**
|
|
* Admin JavaScript for Care Booking Block plugin
|
|
*
|
|
* @package CareBookingBlock
|
|
*/
|
|
|
|
(function($) {
|
|
'use strict';
|
|
|
|
// Global variables
|
|
let currentTab = 'doctors';
|
|
let doctorsData = [];
|
|
let servicesData = [];
|
|
let isLoading = false;
|
|
|
|
/**
|
|
* Initialize admin interface
|
|
*/
|
|
function init() {
|
|
bindEvents();
|
|
loadInitialData();
|
|
updateStatus();
|
|
}
|
|
|
|
/**
|
|
* Bind event handlers
|
|
*/
|
|
function bindEvents() {
|
|
// Tab navigation
|
|
$('.nav-tab').on('click', handleTabClick);
|
|
|
|
// Doctors tab events
|
|
$('#refresh-doctors').on('click', loadDoctors);
|
|
$('#bulk-block-doctors').on('click', () => bulkToggleRestrictions('doctors', true));
|
|
$('#bulk-unblock-doctors').on('click', () => bulkToggleRestrictions('doctors', false));
|
|
$('#select-all-doctors').on('change', toggleAllCheckboxes);
|
|
$('#doctors-search').on('input', debounce(searchDoctors, 300));
|
|
$('#search-doctors').on('click', searchDoctors);
|
|
$(document).on('click', '.toggle-doctor', handleDoctorToggle);
|
|
$(document).on('change', '.doctor-checkbox', updateBulkButtons);
|
|
$(document).on('click', '.view-services', viewDoctorServices);
|
|
|
|
// Services tab events
|
|
$('#services-doctor-filter').on('change', filterServices);
|
|
$('#filter-services').on('click', filterServices);
|
|
$('#refresh-services').on('click', loadServices);
|
|
$('#bulk-block-services').on('click', () => bulkToggleRestrictions('services', true));
|
|
$('#bulk-unblock-services').on('click', () => bulkToggleRestrictions('services', false));
|
|
$('#select-all-services').on('change', toggleAllCheckboxes);
|
|
$(document).on('click', '.toggle-service', handleServiceToggle);
|
|
$(document).on('change', '.service-checkbox', updateBulkButtons);
|
|
|
|
// Settings events
|
|
$('#settings-form').on('submit', saveSettings);
|
|
$('#clear-cache').on('click', clearCache);
|
|
$('#export-settings').on('click', exportSettings);
|
|
$('#import-settings').on('click', () => $('#import-file').click());
|
|
$('#import-file').on('change', importSettings);
|
|
|
|
// Notice dismissal
|
|
$(document).on('click', '.notice-dismiss', hideNotice);
|
|
}
|
|
|
|
/**
|
|
* Handle tab click
|
|
*/
|
|
function handleTabClick(e) {
|
|
e.preventDefault();
|
|
|
|
const tab = $(this).data('tab');
|
|
switchTab(tab);
|
|
}
|
|
|
|
/**
|
|
* Switch to specified tab
|
|
*/
|
|
function switchTab(tab) {
|
|
if (currentTab === tab) return;
|
|
|
|
// Update navigation
|
|
$('.nav-tab').removeClass('nav-tab-active');
|
|
$(`.nav-tab[data-tab="${tab}"]`).addClass('nav-tab-active');
|
|
|
|
// Update content
|
|
$('.tab-content').removeClass('active');
|
|
$(`#${tab}-tab`).addClass('active');
|
|
|
|
currentTab = tab;
|
|
|
|
// Load data for the tab if needed
|
|
if (tab === 'doctors' && doctorsData.length === 0) {
|
|
loadDoctors();
|
|
} else if (tab === 'services' && servicesData.length === 0) {
|
|
loadServices();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load initial data
|
|
*/
|
|
function loadInitialData() {
|
|
loadDoctors();
|
|
loadDoctorFilter();
|
|
loadSettings();
|
|
checkSystemStatus();
|
|
}
|
|
|
|
/**
|
|
* Load doctors data
|
|
*/
|
|
function loadDoctors() {
|
|
if (isLoading) return;
|
|
|
|
// SECURITY: Validate nonce exists before making request
|
|
if (!careBookingAjax.nonce) {
|
|
showError('Security token missing. Please refresh the page.');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
// SECURITY: Enhanced AJAX request with additional validation
|
|
$.post(careBookingAjax.ajaxurl, {
|
|
action: 'care_booking_get_entities',
|
|
entity_type: 'doctors', // Fixed value for security
|
|
nonce: careBookingAjax.nonce
|
|
})
|
|
.done(function(response) {
|
|
// SECURITY: Validate response structure
|
|
if (typeof response !== 'object' || response === null) {
|
|
showError('Invalid server response format.');
|
|
return;
|
|
}
|
|
|
|
if (response.success && response.data && Array.isArray(response.data.entities)) {
|
|
// SECURITY: Sanitize each doctor entry before using
|
|
doctorsData = response.data.entities.map(function(doctor) {
|
|
return {
|
|
id: parseInt(doctor.id) || 0,
|
|
name: escapeHtml(doctor.name || ''),
|
|
email: escapeHtml(doctor.email || ''),
|
|
is_blocked: Boolean(doctor.is_blocked)
|
|
};
|
|
});
|
|
renderDoctors();
|
|
updateStatus();
|
|
} else {
|
|
showError(response.data && response.data.message ? escapeHtml(response.data.message) : 'Failed to load doctors');
|
|
}
|
|
})
|
|
.fail(function(jqXHR, textStatus, errorThrown) {
|
|
// SECURITY: Log error details for debugging but show safe message to user
|
|
console.error('AJAX Error:', textStatus, errorThrown);
|
|
showError(careBookingAjax.strings.error || 'Request failed. Please try again.');
|
|
})
|
|
.always(function() {
|
|
setLoading(false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render doctors list
|
|
*/
|
|
function renderDoctors(filteredData = null) {
|
|
const data = filteredData || doctorsData;
|
|
const template = $('#doctor-row-template').html();
|
|
const tbody = $('#doctors-list');
|
|
|
|
if (data.length === 0) {
|
|
tbody.html('<tr><td colspan="5" class="no-items">No doctors found.</td></tr>');
|
|
return;
|
|
}
|
|
|
|
const rows = data.map(doctor => {
|
|
return template
|
|
.replace(/{{id}}/g, doctor.id)
|
|
.replace(/{{name}}/g, escapeHtml(doctor.name))
|
|
.replace(/{{email}}/g, escapeHtml(doctor.email))
|
|
.replace(/{{status_class}}/g, doctor.is_blocked ? 'blocked' : 'active')
|
|
.replace(/{{status_text}}/g, doctor.is_blocked ? 'Blocked' : 'Active')
|
|
.replace(/{{is_blocked}}/g, doctor.is_blocked ? 'true' : 'false')
|
|
.replace(/{{toggle_icon}}/g, doctor.is_blocked ? 'dashicons-visibility' : 'dashicons-hidden')
|
|
.replace(/{{toggle_text}}/g, doctor.is_blocked ? 'Unblock' : 'Block');
|
|
}).join('');
|
|
|
|
tbody.html(rows);
|
|
updateBulkButtons();
|
|
}
|
|
|
|
/**
|
|
* Load services data
|
|
*/
|
|
function loadServices(doctorId = null) {
|
|
if (isLoading) return;
|
|
|
|
setLoading(true);
|
|
|
|
const data = {
|
|
action: 'care_booking_get_entities',
|
|
entity_type: 'services',
|
|
nonce: careBookingAjax.nonce
|
|
};
|
|
|
|
if (doctorId) {
|
|
data.doctor_id = doctorId;
|
|
}
|
|
|
|
$.post(careBookingAjax.ajaxurl, data)
|
|
.done(function(response) {
|
|
if (response.success) {
|
|
servicesData = response.data.entities;
|
|
renderServices();
|
|
updateStatus();
|
|
} else {
|
|
showError(response.data.message);
|
|
}
|
|
})
|
|
.fail(function() {
|
|
showError(careBookingAjax.strings.error);
|
|
})
|
|
.always(function() {
|
|
setLoading(false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render services list
|
|
*/
|
|
function renderServices(filteredData = null) {
|
|
const data = filteredData || servicesData;
|
|
const template = $('#service-row-template').html();
|
|
const tbody = $('#services-list');
|
|
|
|
if (data.length === 0) {
|
|
tbody.html('<tr><td colspan="5" class="no-items">No services found.</td></tr>');
|
|
return;
|
|
}
|
|
|
|
const rows = data.map(service => {
|
|
const doctorName = getDoctorName(service.doctor_id);
|
|
|
|
return template
|
|
.replace(/{{id}}/g, service.id)
|
|
.replace(/{{doctor_id}}/g, service.doctor_id)
|
|
.replace(/{{name}}/g, escapeHtml(service.name))
|
|
.replace(/{{doctor_name}}/g, escapeHtml(doctorName))
|
|
.replace(/{{status_class}}/g, service.is_blocked ? 'blocked' : 'active')
|
|
.replace(/{{status_text}}/g, service.is_blocked ? 'Blocked' : 'Active')
|
|
.replace(/{{is_blocked}}/g, service.is_blocked ? 'true' : 'false')
|
|
.replace(/{{toggle_icon}}/g, service.is_blocked ? 'dashicons-visibility' : 'dashicons-hidden')
|
|
.replace(/{{toggle_text}}/g, service.is_blocked ? 'Unblock' : 'Block');
|
|
}).join('');
|
|
|
|
tbody.html(rows);
|
|
updateBulkButtons();
|
|
}
|
|
|
|
/**
|
|
* Handle doctor restriction toggle
|
|
*/
|
|
function handleDoctorToggle(e) {
|
|
e.preventDefault();
|
|
|
|
// SECURITY: Rate limiting for toggle actions
|
|
if (!checkActionLimit('toggle_restriction', 20, 60000)) {
|
|
showError('Too many requests. Please wait a moment.');
|
|
return;
|
|
}
|
|
|
|
const $button = $(this);
|
|
const doctorId = $button.data('doctor-id');
|
|
const isBlocked = $button.data('blocked') === true || $button.data('blocked') === 'true';
|
|
const newBlocked = !isBlocked;
|
|
|
|
// SECURITY: Validate doctor ID
|
|
if (!validateNumeric(doctorId)) {
|
|
showError('Invalid doctor ID');
|
|
return;
|
|
}
|
|
|
|
toggleRestriction('doctor', doctorId, null, newBlocked, $button);
|
|
}
|
|
|
|
/**
|
|
* Handle service restriction toggle
|
|
*/
|
|
function handleServiceToggle(e) {
|
|
e.preventDefault();
|
|
|
|
// SECURITY: Rate limiting for toggle actions
|
|
if (!checkActionLimit('toggle_restriction', 20, 60000)) {
|
|
showError('Too many requests. Please wait a moment.');
|
|
return;
|
|
}
|
|
|
|
const $button = $(this);
|
|
const serviceId = $button.data('service-id');
|
|
const doctorId = $button.data('doctor-id');
|
|
const isBlocked = $button.data('blocked') === true || $button.data('blocked') === 'true';
|
|
const newBlocked = !isBlocked;
|
|
|
|
// SECURITY: Validate service and doctor IDs
|
|
if (!validateNumeric(serviceId) || !validateNumeric(doctorId)) {
|
|
showError('Invalid service or doctor ID');
|
|
return;
|
|
}
|
|
|
|
toggleRestriction('service', serviceId, doctorId, newBlocked, $button);
|
|
}
|
|
|
|
/**
|
|
* Toggle single restriction
|
|
*/
|
|
function toggleRestriction(type, targetId, doctorId, isBlocked, $button) {
|
|
// SECURITY: Validate inputs before sending
|
|
if (!type || !targetId || typeof isBlocked !== 'boolean') {
|
|
showError('Invalid restriction parameters');
|
|
return;
|
|
}
|
|
|
|
// SECURITY: Validate nonce
|
|
if (!careBookingAjax.nonce) {
|
|
showError('Security token missing. Please refresh the page.');
|
|
return;
|
|
}
|
|
|
|
// SECURITY: Validate restriction type
|
|
const allowedTypes = ['doctor', 'service'];
|
|
if (!allowedTypes.includes(type)) {
|
|
showError('Invalid restriction type');
|
|
return;
|
|
}
|
|
|
|
const originalText = $button.text();
|
|
$button.prop('disabled', true).text('...');
|
|
|
|
// SECURITY: Sanitize data before sending
|
|
const data = {
|
|
action: 'care_booking_toggle_restriction',
|
|
restriction_type: sanitizeInput(type),
|
|
target_id: parseInt(targetId) || 0,
|
|
is_blocked: Boolean(isBlocked),
|
|
nonce: careBookingAjax.nonce
|
|
};
|
|
|
|
if (doctorId) {
|
|
data.doctor_id = parseInt(doctorId) || 0;
|
|
}
|
|
|
|
$.post(careBookingAjax.ajaxurl, data)
|
|
.done(function(response) {
|
|
if (response.success) {
|
|
updateEntityInData(type, targetId, doctorId, isBlocked);
|
|
|
|
if (type === 'doctor') {
|
|
renderDoctors();
|
|
} else {
|
|
renderServices();
|
|
}
|
|
|
|
updateStatus();
|
|
showSuccess(careBookingAjax.strings.success_update);
|
|
} else {
|
|
showError(response.data.message);
|
|
}
|
|
})
|
|
.fail(function() {
|
|
showError(careBookingAjax.strings.error);
|
|
})
|
|
.always(function() {
|
|
$button.prop('disabled', false).text(originalText);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update entity in local data
|
|
*/
|
|
function updateEntityInData(type, targetId, doctorId, isBlocked) {
|
|
if (type === 'doctor') {
|
|
const doctor = doctorsData.find(d => d.id == targetId);
|
|
if (doctor) {
|
|
doctor.is_blocked = isBlocked;
|
|
}
|
|
} else {
|
|
const service = servicesData.find(s => s.id == targetId && s.doctor_id == doctorId);
|
|
if (service) {
|
|
service.is_blocked = isBlocked;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bulk toggle restrictions
|
|
*/
|
|
function bulkToggleRestrictions(type, isBlocked) {
|
|
// SECURITY: Rate limiting for bulk operations (more restrictive)
|
|
if (!checkActionLimit('bulk_update', 3, 120000)) {
|
|
showError('Too many bulk requests. Please wait 2 minutes.');
|
|
return;
|
|
}
|
|
|
|
const checkboxes = type === 'doctors' ?
|
|
$('.doctor-checkbox:checked') :
|
|
$('.service-checkbox:checked');
|
|
|
|
if (checkboxes.length === 0) {
|
|
showError('Please select items to update.');
|
|
return;
|
|
}
|
|
|
|
// SECURITY: Limit bulk operations size for security
|
|
if (checkboxes.length > 25) {
|
|
showError('Too many items selected. Please select 25 or fewer items.');
|
|
return;
|
|
}
|
|
|
|
if (!confirm(careBookingAjax.strings.confirm_bulk)) {
|
|
return;
|
|
}
|
|
|
|
const restrictions = [];
|
|
|
|
checkboxes.each(function() {
|
|
const $checkbox = $(this);
|
|
const restriction = {
|
|
restriction_type: type.slice(0, -1), // Remove 's'
|
|
target_id: parseInt($checkbox.val()),
|
|
is_blocked: isBlocked
|
|
};
|
|
|
|
if (type === 'services') {
|
|
restriction.doctor_id = parseInt($checkbox.data('doctor-id'));
|
|
}
|
|
|
|
restrictions.push(restriction);
|
|
});
|
|
|
|
bulkUpdate(restrictions);
|
|
}
|
|
|
|
/**
|
|
* Perform bulk update
|
|
*/
|
|
function bulkUpdate(restrictions) {
|
|
setLoading(true);
|
|
|
|
$.post(careBookingAjax.ajaxurl, {
|
|
action: 'care_booking_bulk_update',
|
|
restrictions: restrictions,
|
|
nonce: careBookingAjax.nonce
|
|
})
|
|
.done(function(response) {
|
|
if (response.success || response.data.updated > 0) {
|
|
showSuccess(`${careBookingAjax.strings.success_bulk} Updated: ${response.data.updated}`);
|
|
|
|
if (response.data.errors && response.data.errors.length > 0) {
|
|
const errorMessages = response.data.errors.map(err => err.error).join(', ');
|
|
showError(`Some updates failed: ${errorMessages}`);
|
|
}
|
|
|
|
// Refresh current tab data
|
|
if (currentTab === 'doctors') {
|
|
loadDoctors();
|
|
} else {
|
|
loadServices();
|
|
}
|
|
} else {
|
|
showError(response.data.message);
|
|
}
|
|
})
|
|
.fail(function() {
|
|
showError(careBookingAjax.strings.error);
|
|
})
|
|
.always(function() {
|
|
setLoading(false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle all checkboxes
|
|
*/
|
|
function toggleAllCheckboxes() {
|
|
const $selectAll = $(this);
|
|
const isChecked = $selectAll.is(':checked');
|
|
const checkboxClass = $selectAll.attr('id') === 'select-all-doctors' ?
|
|
'.doctor-checkbox' : '.service-checkbox';
|
|
|
|
$(checkboxClass).prop('checked', isChecked);
|
|
updateBulkButtons();
|
|
}
|
|
|
|
/**
|
|
* Update bulk action buttons state
|
|
*/
|
|
function updateBulkButtons() {
|
|
const doctorsChecked = $('.doctor-checkbox:checked').length;
|
|
const servicesChecked = $('.service-checkbox:checked').length;
|
|
|
|
$('#bulk-block-doctors, #bulk-unblock-doctors')
|
|
.prop('disabled', doctorsChecked === 0);
|
|
|
|
$('#bulk-block-services, #bulk-unblock-services')
|
|
.prop('disabled', servicesChecked === 0);
|
|
}
|
|
|
|
/**
|
|
* Search doctors
|
|
*/
|
|
function searchDoctors() {
|
|
const query = $('#doctors-search').val().toLowerCase();
|
|
|
|
if (!query) {
|
|
renderDoctors();
|
|
return;
|
|
}
|
|
|
|
const filtered = doctorsData.filter(doctor =>
|
|
doctor.name.toLowerCase().includes(query) ||
|
|
doctor.email.toLowerCase().includes(query)
|
|
);
|
|
|
|
renderDoctors(filtered);
|
|
}
|
|
|
|
/**
|
|
* Filter services by doctor
|
|
*/
|
|
function filterServices() {
|
|
const doctorId = $('#services-doctor-filter').val();
|
|
|
|
if (!doctorId) {
|
|
loadServices();
|
|
return;
|
|
}
|
|
|
|
loadServices(parseInt(doctorId));
|
|
}
|
|
|
|
/**
|
|
* View services for specific doctor
|
|
*/
|
|
function viewDoctorServices(e) {
|
|
e.preventDefault();
|
|
|
|
const doctorId = $(this).data('doctor-id');
|
|
|
|
// Switch to services tab
|
|
switchTab('services');
|
|
|
|
// Set doctor filter and load services
|
|
$('#services-doctor-filter').val(doctorId);
|
|
loadServices(doctorId);
|
|
}
|
|
|
|
/**
|
|
* Load doctor filter options
|
|
*/
|
|
function loadDoctorFilter() {
|
|
$.post(careBookingAjax.ajaxurl, {
|
|
action: 'care_booking_get_entities',
|
|
entity_type: 'doctors',
|
|
nonce: careBookingAjax.nonce
|
|
})
|
|
.done(function(response) {
|
|
if (response.success) {
|
|
const $select = $('#services-doctor-filter');
|
|
$select.empty().append('<option value="">All Doctors</option>');
|
|
|
|
response.data.entities.forEach(doctor => {
|
|
$select.append(`<option value="${doctor.id}">${escapeHtml(doctor.name)}</option>`);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Save settings
|
|
*/
|
|
function saveSettings(e) {
|
|
e.preventDefault();
|
|
|
|
const settings = {
|
|
cache_timeout: $('#cache-timeout').val(),
|
|
admin_only: $('#admin-only').is(':checked'),
|
|
css_injection: $('#css-injection').is(':checked')
|
|
};
|
|
|
|
// TODO: Implement settings save AJAX call
|
|
showSuccess('Settings saved successfully.');
|
|
}
|
|
|
|
/**
|
|
* Clear cache
|
|
*/
|
|
function clearCache() {
|
|
if (!confirm('Are you sure you want to clear all plugin caches?')) {
|
|
return;
|
|
}
|
|
|
|
// TODO: Implement cache clear AJAX call
|
|
showSuccess('Cache cleared successfully.');
|
|
updateStatus();
|
|
}
|
|
|
|
/**
|
|
* Export settings
|
|
*/
|
|
function exportSettings() {
|
|
const settings = {
|
|
cache_timeout: $('#cache-timeout').val(),
|
|
admin_only: $('#admin-only').is(':checked'),
|
|
css_injection: $('#css-injection').is(':checked'),
|
|
doctors: doctorsData,
|
|
services: servicesData,
|
|
exported_at: new Date().toISOString()
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(settings, null, 2)], {
|
|
type: 'application/json'
|
|
});
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `care-booking-settings-${new Date().toISOString().slice(0, 10)}.json`;
|
|
a.click();
|
|
|
|
URL.revokeObjectURL(url);
|
|
showSuccess('Settings exported successfully.');
|
|
}
|
|
|
|
/**
|
|
* Import settings
|
|
*/
|
|
function importSettings(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
try {
|
|
const settings = JSON.parse(e.target.result);
|
|
|
|
// Restore settings
|
|
if (settings.cache_timeout) {
|
|
$('#cache-timeout').val(settings.cache_timeout);
|
|
}
|
|
if (typeof settings.admin_only === 'boolean') {
|
|
$('#admin-only').prop('checked', settings.admin_only);
|
|
}
|
|
if (typeof settings.css_injection === 'boolean') {
|
|
$('#css-injection').prop('checked', settings.css_injection);
|
|
}
|
|
|
|
showSuccess('Settings imported successfully.');
|
|
} catch (error) {
|
|
showError('Invalid settings file format.');
|
|
}
|
|
};
|
|
|
|
reader.readAsText(file);
|
|
|
|
// Clear the input
|
|
e.target.value = '';
|
|
}
|
|
|
|
/**
|
|
* Load settings
|
|
*/
|
|
function loadSettings() {
|
|
// TODO: Load settings from server
|
|
$('#cache-timeout').val(3600);
|
|
$('#admin-only').prop('checked', true);
|
|
$('#css-injection').prop('checked', true);
|
|
}
|
|
|
|
/**
|
|
* Check system status
|
|
*/
|
|
function checkSystemStatus() {
|
|
// TODO: Implement real status checks
|
|
$('#kivicare-status').html('<span class="status-badge active">Active</span>');
|
|
$('#database-status').html('<span class="status-badge active">Connected</span>');
|
|
}
|
|
|
|
/**
|
|
* Update status display
|
|
*/
|
|
function updateStatus() {
|
|
const blockedDoctors = doctorsData.filter(d => d.is_blocked).length;
|
|
const blockedServices = servicesData.filter(s => s.is_blocked).length;
|
|
|
|
$('#blocked-doctors-count').text(blockedDoctors);
|
|
$('#blocked-services-count').text(blockedServices);
|
|
$('#cache-status').text('Active');
|
|
}
|
|
|
|
/**
|
|
* Set loading state
|
|
*/
|
|
function setLoading(loading) {
|
|
isLoading = loading;
|
|
|
|
if (loading) {
|
|
$('.care-booking-loading').show();
|
|
} else {
|
|
$('.care-booking-loading').hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show success message
|
|
*/
|
|
function showSuccess(message) {
|
|
showNotice('success', message);
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
*/
|
|
function showError(message) {
|
|
showNotice('error', message);
|
|
}
|
|
|
|
/**
|
|
* Show info message
|
|
*/
|
|
function showInfo(message) {
|
|
showNotice('info', message);
|
|
}
|
|
|
|
/**
|
|
* Show notice
|
|
*/
|
|
function showNotice(type, message) {
|
|
const $notice = $(`#${type}-notice`);
|
|
$notice.find('.message').text(message);
|
|
$('.care-booking-notices').show();
|
|
$notice.show();
|
|
|
|
// Auto-hide after 5 seconds
|
|
setTimeout(() => {
|
|
$notice.fadeOut();
|
|
}, 5000);
|
|
}
|
|
|
|
/**
|
|
* Hide notice
|
|
*/
|
|
function hideNotice() {
|
|
$(this).closest('.notice').fadeOut();
|
|
}
|
|
|
|
/**
|
|
* Get doctor name by ID
|
|
*/
|
|
function getDoctorName(doctorId) {
|
|
const doctor = doctorsData.find(d => d.id == doctorId);
|
|
return doctor ? doctor.name : `Doctor ${doctorId}`;
|
|
}
|
|
|
|
/**
|
|
* Escape HTML
|
|
*/
|
|
function escapeHtml(text) {
|
|
if (typeof text !== 'string') {
|
|
return '';
|
|
}
|
|
const map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return text.replace(/[&<>"']/g, m => map[m]);
|
|
}
|
|
|
|
/**
|
|
* SECURITY: Sanitize input for safe transmission
|
|
*/
|
|
function sanitizeInput(input) {
|
|
if (typeof input !== 'string') {
|
|
return '';
|
|
}
|
|
// Remove potentially dangerous characters
|
|
return input.replace(/[<>'"&]/g, '').trim();
|
|
}
|
|
|
|
/**
|
|
* SECURITY: Validate numeric input
|
|
*/
|
|
function validateNumeric(value, min = 1, max = Number.MAX_SAFE_INTEGER) {
|
|
const num = parseInt(value);
|
|
return !isNaN(num) && num >= min && num <= max;
|
|
}
|
|
|
|
/**
|
|
* SECURITY: Rate limiting for user actions
|
|
*/
|
|
const actionLimits = {};
|
|
function checkActionLimit(action, limit = 10, timeWindow = 60000) {
|
|
const now = Date.now();
|
|
const key = action;
|
|
|
|
if (!actionLimits[key]) {
|
|
actionLimits[key] = [];
|
|
}
|
|
|
|
// Remove old entries
|
|
actionLimits[key] = actionLimits[key].filter(time => now - time < timeWindow);
|
|
|
|
if (actionLimits[key].length >= limit) {
|
|
return false;
|
|
}
|
|
|
|
actionLimits[key].push(now);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Debounce function
|
|
*/
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Initialize when document is ready
|
|
$(document).ready(init);
|
|
|
|
})(jQuery); |