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:
844
care-booking-block/admin/js/admin-script.js
Normal file
844
care-booking-block/admin/js/admin-script.js
Normal file
@@ -0,0 +1,844 @@
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user