- 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>
6 lines
16 KiB
JavaScript
6 lines
16 KiB
JavaScript
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
(function($) { 'use strict'; let currentTab = 'doctors'; let doctorsData = []; let servicesData = []; let isLoading = false; function init() { bindEvents(); loadInitialData(); updateStatus(); } function bindEvents() { $('.nav-tab').on('click', handleTabClick); $('#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-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-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); $(document).on('click', '.notice-dismiss', hideNotice); } function handleTabClick(e) { e.preventDefault(); const tab = $(this).data('tab'); switchTab(tab); } function switchTab(tab) { if (currentTab === tab) return; $('.nav-tab').removeClass('nav-tab-active'); $(`.nav-tab[data-tab="${tab}"]`).addClass('nav-tab-active'); $('.tab-content').removeClass('active'); $(`#${tab}-tab`).addClass('active'); currentTab = tab; if (tab === 'doctors' && doctorsData.length === 0) { loadDoctors(); } else if (tab === 'services' && servicesData.length === 0) { loadServices(); } } function loadInitialData() { loadDoctors(); loadDoctorFilter(); loadSettings(); checkSystemStatus(); } function loadDoctors() { if (isLoading) return; if (!careBookingAjax.nonce) { showError('Security token missing. Please refresh the page.'); return; } setLoading(true); $.post(careBookingAjax.ajaxurl, { action: 'care_booking_get_entities', entity_type: 'doctors', nonce: careBookingAjax.nonce }) .done(function(response) { if (typeof response !== 'object' || response === null) { showError('Invalid server response format.'); return; } if (response.success && response.data && Array.isArray(response.data.entities)) { 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) { console.error('AJAX Error:', textStatus, errorThrown); showError(careBookingAjax.strings.error || 'Request failed. Please try again.'); }) .always(function() { setLoading(false); }); } 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(); } 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); }); } 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(); } function handleDoctorToggle(e) { e.preventDefault(); 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; if (!validateNumeric(doctorId)) { showError('Invalid doctor ID'); return; } toggleRestriction('doctor', doctorId, null, newBlocked, $button); } function handleServiceToggle(e) { e.preventDefault(); 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; if (!validateNumeric(serviceId) || !validateNumeric(doctorId)) { showError('Invalid service or doctor ID'); return; } toggleRestriction('service', serviceId, doctorId, newBlocked, $button); } function toggleRestriction(type, targetId, doctorId, isBlocked, $button) { if (!type || !targetId || typeof isBlocked !== 'boolean') { showError('Invalid restriction parameters'); return; } if (!careBookingAjax.nonce) { showError('Security token missing. Please refresh the page.'); return; } const allowedTypes = ['doctor', 'service']; if (!allowedTypes.includes(type)) { showError('Invalid restriction type'); return; } const originalText = $button.text(); $button.prop('disabled', true).text('...'); 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); }); } 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; } } } function bulkToggleRestrictions(type, isBlocked) { 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; } 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), target_id: parseInt($checkbox.val()), is_blocked: isBlocked }; if (type === 'services') { restriction.doctor_id = parseInt($checkbox.data('doctor-id')); } restrictions.push(restriction); }); bulkUpdate(restrictions); } 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}`); } if (currentTab === 'doctors') { loadDoctors(); } else { loadServices(); } } else { showError(response.data.message); } }) .fail(function() { showError(careBookingAjax.strings.error); }) .always(function() { setLoading(false); }); } 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(); } 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); } 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); } function filterServices() { const doctorId = $('#services-doctor-filter').val(); if (!doctorId) { loadServices(); return; } loadServices(parseInt(doctorId)); } function viewDoctorServices(e) { e.preventDefault(); const doctorId = $(this).data('doctor-id'); switchTab('services'); $('#services-doctor-filter').val(doctorId); loadServices(doctorId); } 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>`); }); } }); } function saveSettings(e) { e.preventDefault(); const settings = { cache_timeout: $('#cache-timeout').val(), admin_only: $('#admin-only').is(':checked'), css_injection: $('#css-injection').is(':checked') }; showSuccess('Settings saved successfully.'); } function clearCache() { if (!confirm('Are you sure you want to clear all plugin caches?')) { return; } showSuccess('Cache cleared successfully.'); updateStatus(); } 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.'); } 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); 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); e.target.value = ''; } function loadSettings() { $('#cache-timeout').val(3600); $('#admin-only').prop('checked', true); $('#css-injection').prop('checked', true); } function checkSystemStatus() { $('#kivicare-status').html('<span class="status-badge active">Active</span>'); $('#database-status').html('<span class="status-badge active">Connected</span>'); } 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'); } function setLoading(loading) { isLoading = loading; if (loading) { $('.care-booking-loading').show(); } else { $('.care-booking-loading').hide(); } } function showSuccess(message) { showNotice('success', message); } function showError(message) { showNotice('error', message); } function showInfo(message) { showNotice('info', message); } function showNotice(type, message) { const $notice = $(`#${type}-notice`); $notice.find('.message').text(message); $('.care-booking-notices').show(); $notice.show(); setTimeout(() => { $notice.fadeOut(); }, 5000); } function hideNotice() { $(this).closest('.notice').fadeOut(); } function getDoctorName(doctorId) { const doctor = doctorsData.find(d => d.id == doctorId); return doctor ? doctor.name : `Doctor ${doctorId}`; } function escapeHtml(text) { if (typeof text !== 'string') { return ''; } const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } function sanitizeInput(input) { if (typeof input !== 'string') { return ''; } return input.replace(/[<>'"&]/g, '').trim(); } function validateNumeric(value, min = 1, max = Number.MAX_SAFE_INTEGER) { const num = parseInt(value); return !isNaN(num) && num >= min && num <= max; } const actionLimits = {}; function checkActionLimit(action, limit = 10, timeWindow = 60000) { const now = Date.now(); const key = action; if (!actionLimits[key]) { actionLimits[key] = []; } actionLimits[key] = actionLimits[key].filter(time => now - time < timeWindow); if (actionLimits[key].length >= limit) { return false; } actionLimits[key].push(now); return true; } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } $(document).ready(init); })(jQuery); |