Files
care-book-block-ultimate/care-booking-block/admin/js/admin-script.js
Emanuel Almeida 38bb926742 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>
2025-09-12 01:27:34 +01:00

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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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);