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:
476
care-booking-block/admin/css/admin-style.css
Normal file
476
care-booking-block/admin/css/admin-style.css
Normal 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);
|
||||
}
|
||||
6
care-booking-block/admin/css/admin-style.min.css
vendored
Normal file
6
care-booking-block/admin/css/admin-style.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
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);
|
||||
6
care-booking-block/admin/js/admin-script.min.js
vendored
Normal file
6
care-booking-block/admin/js/admin-script.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
336
care-booking-block/admin/partials/admin-display.php
Normal file
336
care-booking-block/admin/partials/admin-display.php
Normal 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>
|
||||
Reference in New Issue
Block a user