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:
Emanuel Almeida
2025-09-12 01:27:34 +01:00
parent 4a60be990e
commit 38bb926742
118 changed files with 40694 additions and 0 deletions

View File

@@ -0,0 +1,476 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
/**
* Admin CSS for Care Booking Block plugin
*
* @package CareBookingBlock
*/
/* Main Admin Container */
.care-booking-admin {
margin: 20px 0;
}
.care-booking-admin h1 {
margin-bottom: 30px;
color: #1e1e1e;
font-size: 23px;
font-weight: 400;
line-height: 1.3;
}
/* Status Banner */
.care-booking-status {
display: flex;
gap: 20px;
margin-bottom: 30px;
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
padding: 20px;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.status-item {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.status-item .dashicons {
font-size: 20px;
width: 20px;
height: 20px;
color: #646970;
}
.status-item strong {
font-size: 24px;
font-weight: 600;
color: #1e1e1e;
}
.status-item span:last-child {
color: #646970;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Navigation Tabs */
.nav-tab-wrapper {
margin-bottom: 20px;
}
.nav-tab {
position: relative;
display: inline-block;
padding: 8px 14px;
margin: 0;
text-decoration: none;
color: #646970;
font-size: 14px;
line-height: 1.4;
border: 1px solid transparent;
border-bottom-color: #c3c4c7;
background: transparent;
cursor: pointer;
}
.nav-tab:hover {
color: #135e96;
}
.nav-tab-active {
color: #1e1e1e;
background-color: #fff;
border-color: #c3c4c7 #c3c4c7 #fff;
border-bottom: 1px solid #fff;
margin-bottom: -1px;
}
/* Tab Content */
.tab-content {
display: none;
background: #fff;
border: 1px solid #c3c4c7;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.tab-content.active {
display: block;
}
/* Table Styling */
.wp-list-table {
border: 0;
box-shadow: none;
}
.wp-list-table th,
.wp-list-table td {
padding: 12px 10px;
vertical-align: middle;
}
.wp-list-table .column-cb {
width: 2.2em;
}
.wp-list-table .column-name {
width: 35%;
}
.wp-list-table .column-email {
width: 25%;
}
.wp-list-table .column-doctor {
width: 25%;
}
.wp-list-table .column-status {
width: 15%;
}
.wp-list-table .column-actions {
width: 15%;
}
/* Status Badges */
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-badge.blocked {
background-color: #d63638;
color: #fff;
}
.status-badge.active {
background-color: #00a32a;
color: #fff;
}
.status-badge.unknown {
background-color: #646970;
color: #fff;
}
.status-badge.checking {
background-color: #f0f0f1;
color: #646970;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Action Buttons */
.button {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
line-height: 1.4;
padding: 6px 10px;
}
.button .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Table Navigation */
.tablenav {
padding: 10px 0;
background: #fff;
border-bottom: 1px solid #c3c4c7;
}
.tablenav .alignleft {
float: left;
}
.tablenav .alignright {
float: right;
}
.tablenav .alignleft .button,
.tablenav .alignright .button {
margin-right: 5px;
}
.tablenav select {
margin-right: 5px;
}
.tablenav input[type="search"] {
width: 200px;
margin-right: 5px;
}
/* Loading Indicator */
.care-booking-loading {
text-align: center;
padding: 40px 20px;
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
margin: 20px 0;
}
.care-booking-loading .spinner {
float: none;
margin: 0 auto 10px;
}
.care-booking-loading p {
margin: 0;
color: #646970;
}
/* Form Styling */
.form-table th {
width: 200px;
padding: 15px 10px 15px 0;
}
.form-table td {
padding: 15px 10px;
}
.form-table input[type="number"] {
width: 100px;
}
.form-table .description {
margin-top: 5px;
color: #646970;
font-style: italic;
}
/* Settings Actions */
.settings-actions {
padding: 20px 0;
border-top: 1px solid #c3c4c7;
margin-top: 20px;
}
.settings-actions .button {
margin-right: 10px;
}
/* System Information */
.system-info {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #c3c4c7;
}
.system-info h3 {
margin-bottom: 15px;
color: #1e1e1e;
}
.system-info .wp-list-table {
max-width: 600px;
}
.system-info th {
font-weight: 600;
width: 200px;
}
/* Notices */
.care-booking-notices {
margin: 20px 0;
}
.care-booking-notices .notice {
margin: 5px 0;
padding: 12px;
}
.care-booking-notices .notice p {
margin: 0;
}
/* Row Actions */
.row-actions {
visibility: hidden;
padding: 2px 0 0;
color: #646970;
}
tr:hover .row-actions {
visibility: visible;
}
.row-actions span {
display: inline;
}
.row-actions a {
color: #2271b1;
text-decoration: none;
}
.row-actions a:hover {
color: #135e96;
}
/* Responsive Design */
@media screen and (max-width: 782px) {
.care-booking-status {
flex-direction: column;
gap: 15px;
}
.status-item {
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f0f0f1;
}
.status-item:last-child {
border-bottom: none;
}
.tablenav .alignleft,
.tablenav .alignright {
float: none;
margin-bottom: 10px;
}
.tablenav input[type="search"] {
width: 100%;
max-width: 280px;
}
.wp-list-table .column-email,
.wp-list-table .column-doctor {
display: none;
}
.wp-list-table .column-name {
width: 60%;
}
.wp-list-table .column-status {
width: 25%;
}
.wp-list-table .column-actions {
width: 15%;
}
}
@media screen and (max-width: 600px) {
.care-booking-admin h1 {
font-size: 20px;
}
.status-item strong {
font-size: 20px;
}
.settings-actions .button {
display: block;
width: 100%;
margin: 0 0 10px 0;
text-align: center;
}
.system-info .wp-list-table th {
width: 120px;
font-size: 13px;
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.care-booking-admin h1 {
color: #f0f0f1;
}
.status-item .dashicons,
.status-item span:last-child {
color: #a7aaad;
}
.status-item strong {
color: #f0f0f1;
}
.care-booking-loading p {
color: #a7aaad;
}
}
/* Accessibility Improvements */
.screen-reader-text {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
-webkit-clip-path: inset(50%);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
word-wrap: normal;
}
.button:focus,
.nav-tab:focus {
box-shadow: 0 0 0 2px #2271b1;
outline: none;
}
.status-badge:focus-visible {
outline: 2px solid #2271b1;
outline-offset: 2px;
}
/* Animation for smooth transitions */
.tab-content {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.status-badge {
transition: all 0.2s ease-in-out;
}
.button {
transition: all 0.2s ease-in-out;
}
.button:hover {
transform: translateY(-1px);
}

File diff suppressed because one or more lines are too long

View 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 = {
'&': '&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);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,336 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Admin display for Care Booking Block plugin
*
* @package CareBookingBlock
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap care-booking-admin">
<h1><?php esc_html_e('Care Booking Control', 'care-booking-block'); ?></h1>
<!-- Status Banner -->
<div class="care-booking-status">
<div class="status-item">
<span class="dashicons dashicons-admin-users"></span>
<strong id="blocked-doctors-count">0</strong>
<span><?php esc_html_e('Blocked Doctors', 'care-booking-block'); ?></span>
</div>
<div class="status-item">
<span class="dashicons dashicons-admin-settings"></span>
<strong id="blocked-services-count">0</strong>
<span><?php esc_html_e('Blocked Services', 'care-booking-block'); ?></span>
</div>
<div class="status-item">
<span class="dashicons dashicons-performance"></span>
<strong id="cache-status"><?php esc_html_e('Unknown', 'care-booking-block'); ?></strong>
<span><?php esc_html_e('Cache Status', 'care-booking-block'); ?></span>
</div>
</div>
<!-- Navigation Tabs -->
<nav class="nav-tab-wrapper wp-clearfix">
<a href="#doctors" class="nav-tab nav-tab-active" data-tab="doctors">
<?php esc_html_e('Doctors', 'care-booking-block'); ?>
</a>
<a href="#services" class="nav-tab" data-tab="services">
<?php esc_html_e('Services', 'care-booking-block'); ?>
</a>
<a href="#settings" class="nav-tab" data-tab="settings">
<?php esc_html_e('Settings', 'care-booking-block'); ?>
</a>
</nav>
<!-- Loading Indicator -->
<div class="care-booking-loading" style="display: none;">
<div class="spinner is-active"></div>
<p><?php esc_html_e('Loading...', 'care-booking-block'); ?></p>
</div>
<!-- Doctors Tab -->
<div id="doctors-tab" class="tab-content active">
<div class="tablenav top">
<div class="alignleft actions">
<button type="button" class="button" id="refresh-doctors">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e('Refresh', 'care-booking-block'); ?>
</button>
<button type="button" class="button" id="bulk-block-doctors">
<span class="dashicons dashicons-hidden"></span>
<?php esc_html_e('Block Selected', 'care-booking-block'); ?>
</button>
<button type="button" class="button" id="bulk-unblock-doctors">
<span class="dashicons dashicons-visibility"></span>
<?php esc_html_e('Unblock Selected', 'care-booking-block'); ?>
</button>
</div>
<div class="alignright actions">
<input type="search" id="doctors-search" placeholder="<?php esc_attr_e('Search doctors...', 'care-booking-block'); ?>" />
<button type="button" class="button" id="search-doctors"><?php esc_html_e('Search', 'care-booking-block'); ?></button>
</div>
</div>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<td class="manage-column column-cb check-column">
<input type="checkbox" id="select-all-doctors" />
</td>
<th class="manage-column column-name column-primary">
<?php esc_html_e('Doctor Name', 'care-booking-block'); ?>
</th>
<th class="manage-column column-email">
<?php esc_html_e('Email', 'care-booking-block'); ?>
</th>
<th class="manage-column column-status">
<?php esc_html_e('Status', 'care-booking-block'); ?>
</th>
<th class="manage-column column-actions">
<?php esc_html_e('Actions', 'care-booking-block'); ?>
</th>
</tr>
</thead>
<tbody id="doctors-list">
<tr>
<td colspan="5" class="no-items">
<?php esc_html_e('No doctors found. Loading...', 'care-booking-block'); ?>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Services Tab -->
<div id="services-tab" class="tab-content">
<div class="tablenav top">
<div class="alignleft actions">
<select id="services-doctor-filter">
<option value=""><?php esc_html_e('All Doctors', 'care-booking-block'); ?></option>
</select>
<button type="button" class="button" id="filter-services">
<?php esc_html_e('Filter', 'care-booking-block'); ?>
</button>
<button type="button" class="button" id="refresh-services">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e('Refresh', 'care-booking-block'); ?>
</button>
</div>
<div class="alignright actions">
<button type="button" class="button" id="bulk-block-services">
<span class="dashicons dashicons-hidden"></span>
<?php esc_html_e('Block Selected', 'care-booking-block'); ?>
</button>
<button type="button" class="button" id="bulk-unblock-services">
<span class="dashicons dashicons-visibility"></span>
<?php esc_html_e('Unblock Selected', 'care-booking-block'); ?>
</button>
</div>
</div>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<td class="manage-column column-cb check-column">
<input type="checkbox" id="select-all-services" />
</td>
<th class="manage-column column-name column-primary">
<?php esc_html_e('Service Name', 'care-booking-block'); ?>
</th>
<th class="manage-column column-doctor">
<?php esc_html_e('Doctor', 'care-booking-block'); ?>
</th>
<th class="manage-column column-status">
<?php esc_html_e('Status', 'care-booking-block'); ?>
</th>
<th class="manage-column column-actions">
<?php esc_html_e('Actions', 'care-booking-block'); ?>
</th>
</tr>
</thead>
<tbody id="services-list">
<tr>
<td colspan="5" class="no-items">
<?php esc_html_e('No services found. Select a doctor or click refresh.', 'care-booking-block'); ?>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Settings Tab -->
<div id="settings-tab" class="tab-content">
<form id="settings-form">
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row">
<label for="cache-timeout">
<?php esc_html_e('Cache Timeout', 'care-booking-block'); ?>
</label>
</th>
<td>
<input type="number" id="cache-timeout" name="cache_timeout" value="3600" min="300" max="86400" />
<p class="description">
<?php esc_html_e('Cache timeout in seconds (300-86400). Default: 3600 (1 hour).', 'care-booking-block'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="admin-only">
<?php esc_html_e('Admin Only Mode', 'care-booking-block'); ?>
</label>
</th>
<td>
<input type="checkbox" id="admin-only" name="admin_only" />
<label for="admin-only">
<?php esc_html_e('Only apply restrictions on frontend (keep full access in admin)', 'care-booking-block'); ?>
</label>
</td>
</tr>
<tr>
<th scope="row">
<label for="css-injection">
<?php esc_html_e('CSS Injection', 'care-booking-block'); ?>
</label>
</th>
<td>
<input type="checkbox" id="css-injection" name="css_injection" checked />
<label for="css-injection">
<?php esc_html_e('Enable CSS injection to hide blocked elements', 'care-booking-block'); ?>
</label>
</td>
</tr>
</tbody>
</table>
<div class="settings-actions">
<button type="submit" class="button-primary">
<?php esc_html_e('Save Settings', 'care-booking-block'); ?>
</button>
<button type="button" class="button" id="clear-cache">
<span class="dashicons dashicons-trash"></span>
<?php esc_html_e('Clear Cache', 'care-booking-block'); ?>
</button>
<button type="button" class="button" id="export-settings">
<span class="dashicons dashicons-download"></span>
<?php esc_html_e('Export Settings', 'care-booking-block'); ?>
</button>
<button type="button" class="button" id="import-settings">
<span class="dashicons dashicons-upload"></span>
<?php esc_html_e('Import Settings', 'care-booking-block'); ?>
</button>
</div>
</form>
<!-- System Information -->
<div class="system-info">
<h3><?php esc_html_e('System Information', 'care-booking-block'); ?></h3>
<table class="wp-list-table widefat">
<tbody>
<tr>
<th><?php esc_html_e('Plugin Version', 'care-booking-block'); ?></th>
<td><?php echo esc_html(CARE_BOOKING_BLOCK_VERSION); ?></td>
</tr>
<tr>
<th><?php esc_html_e('WordPress Version', 'care-booking-block'); ?></th>
<td><?php echo esc_html(get_bloginfo('version')); ?></td>
</tr>
<tr>
<th><?php esc_html_e('PHP Version', 'care-booking-block'); ?></th>
<td><?php echo esc_html(PHP_VERSION); ?></td>
</tr>
<tr>
<th><?php esc_html_e('KiviCare Status', 'care-booking-block'); ?></th>
<td id="kivicare-status">
<span class="status-checking"><?php esc_html_e('Checking...', 'care-booking-block'); ?></span>
</td>
</tr>
<tr>
<th><?php esc_html_e('Database Table', 'care-booking-block'); ?></th>
<td id="database-status">
<span class="status-checking"><?php esc_html_e('Checking...', 'care-booking-block'); ?></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Success/Error Messages -->
<div class="care-booking-notices" style="display: none;">
<div class="notice notice-success is-dismissible" id="success-notice" style="display: none;">
<p><strong><?php esc_html_e('Success!', 'care-booking-block'); ?></strong> <span class="message"></span></p>
</div>
<div class="notice notice-error is-dismissible" id="error-notice" style="display: none;">
<p><strong><?php esc_html_e('Error!', 'care-booking-block'); ?></strong> <span class="message"></span></p>
</div>
<div class="notice notice-info is-dismissible" id="info-notice" style="display: none;">
<p><strong><?php esc_html_e('Info!', 'care-booking-block'); ?></strong> <span class="message"></span></p>
</div>
</div>
<!-- Hidden File Input for Import -->
<input type="file" id="import-file" accept=".json" style="display: none;" />
</div>
<!-- Doctor Row Template -->
<script type="text/template" id="doctor-row-template">
<tr data-doctor-id="{{id}}">
<th scope="row" class="check-column">
<input type="checkbox" class="doctor-checkbox" value="{{id}}" />
</th>
<td class="column-name column-primary">
<strong>{{name}}</strong>
<div class="row-actions">
<span class="view">
<a href="#" class="view-services" data-doctor-id="{{id}}">
<?php esc_html_e('View Services', 'care-booking-block'); ?>
</a>
</span>
</div>
</td>
<td class="column-email">{{email}}</td>
<td class="column-status">
<span class="status-badge {{status_class}}">{{status_text}}</span>
</td>
<td class="column-actions">
<button type="button" class="button toggle-doctor" data-doctor-id="{{id}}" data-blocked="{{is_blocked}}">
<span class="dashicons {{toggle_icon}}"></span>
{{toggle_text}}
</button>
</td>
</tr>
</script>
<!-- Service Row Template -->
<script type="text/template" id="service-row-template">
<tr data-service-id="{{id}}" data-doctor-id="{{doctor_id}}">
<th scope="row" class="check-column">
<input type="checkbox" class="service-checkbox" value="{{id}}" data-doctor-id="{{doctor_id}}" />
</th>
<td class="column-name column-primary">
<strong>{{name}}</strong>
</td>
<td class="column-doctor">{{doctor_name}}</td>
<td class="column-status">
<span class="status-badge {{status_class}}">{{status_text}}</span>
</td>
<td class="column-actions">
<button type="button" class="button toggle-service" data-service-id="{{id}}" data-doctor-id="{{doctor_id}}" data-blocked="{{is_blocked}}">
<span class="dashicons {{toggle_icon}}"></span>
{{toggle_text}}
</button>
</td>
</tr>
</script>