Add schedule preview dropdown styles and various improvements
- Add .schedule-preview-dropdown and .schedule-preview-item CSS classes - Add .btn-schedule-preview badge styling - Add preview functionality for entity list views - Improve modal and dropdown styling - Various JS and SCSS enhancements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8579,6 +8579,67 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.schedule-preview-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 4px;
|
||||
z-index: 1000;
|
||||
min-width: 180px;
|
||||
padding: 0.5rem;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.schedule-preview-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
color: #212529;
|
||||
}
|
||||
.schedule-preview-item .material-icons {
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
color: #6c757d;
|
||||
}
|
||||
.schedule-preview-item strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.schedule-preview-item .schedule-time {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-schedule-preview {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 0.5rem;
|
||||
background: #25b9d7;
|
||||
color: #ffffff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 50rem;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
}
|
||||
.btn-schedule-preview .icon-eye {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity Selector Styles
|
||||
* @package prestashop-entity-selector
|
||||
|
||||
File diff suppressed because one or more lines are too long
10186
assets/js/admin/entity-selector.js.bak
Normal file
10186
assets/js/admin/entity-selector.js.bak
Normal file
File diff suppressed because it is too large
Load Diff
@@ -39,9 +39,9 @@ const paths = {
|
||||
function scssTask() {
|
||||
return gulp.src(paths.scss.src)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({
|
||||
outputStyle: 'compressed',
|
||||
charset: false // Prevent UTF-8 BOM in output
|
||||
.pipe(sass.sync({
|
||||
style: 'compressed',
|
||||
silenceDeprecations: ['legacy-js-api']
|
||||
}).on('error', sass.logError))
|
||||
.pipe(rename('entity-selector.css'))
|
||||
.pipe(sourcemaps.write('.'))
|
||||
|
||||
@@ -55,7 +55,29 @@
|
||||
priceMin: null,
|
||||
priceMax: null,
|
||||
attributes: [],
|
||||
features: []
|
||||
features: [],
|
||||
// Entity-specific filters
|
||||
productCountMin: null,
|
||||
productCountMax: null,
|
||||
salesMin: null,
|
||||
salesMax: null,
|
||||
turnoverMin: null,
|
||||
turnoverMax: null,
|
||||
depth: null,
|
||||
hasProducts: false,
|
||||
hasDescription: false,
|
||||
hasImage: false,
|
||||
activeOnly: true, // Default to active only
|
||||
attributeGroup: null,
|
||||
featureGroup: null,
|
||||
dateAddFrom: null,
|
||||
dateAddTo: null,
|
||||
lastProductFrom: null,
|
||||
lastProductTo: null,
|
||||
// Country-specific filters
|
||||
hasHolidays: false,
|
||||
containsStates: false,
|
||||
zone: null
|
||||
},
|
||||
filterableData: null,
|
||||
// Search history
|
||||
@@ -102,10 +124,20 @@
|
||||
this.$wrapper.find('.group-modifiers').hide();
|
||||
}
|
||||
|
||||
// Add fullwidth class to parent form-group
|
||||
var $formGroup = this.$wrapper.closest('.form-group');
|
||||
$formGroup.addClass('condition-trait-fullwidth');
|
||||
$formGroup.find('.col-lg-offset-3').removeClass('col-lg-offset-3');
|
||||
// Add fullwidth class to parent form-group (skip for form-group layout)
|
||||
var hasLayoutFormGroup = this.$wrapper.hasClass('layout-form-group');
|
||||
var $entitySelectorFormGroup = this.$wrapper.closest('.entity-selector-form-group');
|
||||
console.log('[EntitySelector] hasLayoutFormGroup:', hasLayoutFormGroup);
|
||||
console.log('[EntitySelector] closest .entity-selector-form-group:', $entitySelectorFormGroup.length);
|
||||
|
||||
if (!hasLayoutFormGroup && !$entitySelectorFormGroup.length) {
|
||||
console.log('[EntitySelector] Adding condition-trait-fullwidth to form-group');
|
||||
var $formGroup = this.$wrapper.closest('.form-group');
|
||||
$formGroup.addClass('condition-trait-fullwidth');
|
||||
$formGroup.find('.col-lg-offset-3').removeClass('col-lg-offset-3');
|
||||
} else {
|
||||
console.log('[EntitySelector] SKIPPING fullwidth - form-group layout detected');
|
||||
}
|
||||
|
||||
this.createDropdown();
|
||||
this.bindEvents();
|
||||
|
||||
@@ -327,6 +327,20 @@
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
|
||||
// Entity-specific filters: Countries
|
||||
html += '<div class="filter-row filter-row-entity-countries" data-entity="countries" style="display:none;">';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-has-holidays"> ' + (trans.has_holidays || 'Has holidays') + '</label>';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-contains-states"> ' + (trans.contains_states || 'Has states') + '</label>';
|
||||
html += '<div class="filter-select-group">';
|
||||
html += '<span class="filter-select-label"><i class="icon-globe"></i> ' + (trans.zone || 'Zone') + ':</span>';
|
||||
html += '<select class="filter-zone-select">';
|
||||
html += '<option value="">' + (trans.all_zones || 'All zones') + '</option>';
|
||||
html += '</select>';
|
||||
html += '</div>';
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>'; // End filter-panel
|
||||
|
||||
// Results header for list view (product columns)
|
||||
|
||||
@@ -101,14 +101,18 @@
|
||||
// Close popover when clicking outside
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('.target-preview-popover').length &&
|
||||
!$(e.target).closest('.holiday-preview-popover').length &&
|
||||
!$(e.target).closest('.tab-badge').length &&
|
||||
!$(e.target).closest('.condition-match-count').length &&
|
||||
!$(e.target).closest('.group-count-badge').length &&
|
||||
!$(e.target).closest('.group-modifiers').length &&
|
||||
!$(e.target).closest('.group-preview-badge').length &&
|
||||
!$(e.target).closest('.toggle-count.clickable').length &&
|
||||
!$(e.target).closest('.trait-total-count').length) {
|
||||
!$(e.target).closest('.trait-total-count').length &&
|
||||
!$(e.target).closest('.chip-preview-holidays').length) {
|
||||
self.hidePreviewPopover();
|
||||
// Also close holiday popover
|
||||
$('.holiday-preview-popover').remove();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -131,6 +135,23 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle blocks content (form-content layout)
|
||||
this.$wrapper.on('click', '.btn-toggle-blocks', function(e) {
|
||||
e.preventDefault();
|
||||
var $blocksContent = self.$wrapper.find('.entity-selector-blocks-content');
|
||||
var $icon = $(this).find('.material-icons');
|
||||
$blocksContent.stop(true, true);
|
||||
if ($blocksContent.is(':visible')) {
|
||||
$blocksContent.slideUp(200);
|
||||
self.$wrapper.addClass('blocks-collapsed');
|
||||
$icon.text('expand_more');
|
||||
} else {
|
||||
$blocksContent.slideDown(200);
|
||||
self.$wrapper.removeClass('blocks-collapsed');
|
||||
$icon.text('expand_less');
|
||||
}
|
||||
});
|
||||
|
||||
// Group-level collapse toggle (click on group header or toggle icon)
|
||||
this.$wrapper.on('click', '.group-header', function(e) {
|
||||
if ($(e.target).closest('.btn-remove-group, .group-name-input').length) {
|
||||
@@ -1057,6 +1078,8 @@
|
||||
var $row = $(this).closest('.group-include, .exclude-row');
|
||||
var id = $chip.data('id');
|
||||
|
||||
console.log('[EntitySelector] Chip remove clicked, id:', id);
|
||||
|
||||
// Also remove from pending selections if dropdown is open
|
||||
if (self.pendingSelections) {
|
||||
self.pendingSelections = self.pendingSelections.filter(function(s) {
|
||||
@@ -1065,6 +1088,7 @@
|
||||
}
|
||||
|
||||
self.removeSelection($picker, id);
|
||||
console.log('[EntitySelector] Calling serializeAllBlocks after chip remove');
|
||||
self.serializeAllBlocks($row);
|
||||
|
||||
if (self.$dropdown && self.$dropdown.hasClass('show')) {
|
||||
@@ -1072,6 +1096,20 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Country chip holiday preview
|
||||
this.$wrapper.on('click', '.chip-preview-holidays', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
var $btn = $(this);
|
||||
var $chip = $btn.closest('.entity-chip');
|
||||
var countryId = $chip.data('id');
|
||||
var countryName = $chip.find('.chip-name').text();
|
||||
var countryIso = $chip.data('iso') || '';
|
||||
|
||||
self.showHolidayPreview(countryId, countryName, countryIso, $btn);
|
||||
});
|
||||
|
||||
// Chips show more/less toggle
|
||||
this.$wrapper.on('click', '.chips-show-more-toggle', function(e) {
|
||||
e.stopPropagation();
|
||||
@@ -1730,8 +1768,26 @@
|
||||
self.refreshSearch();
|
||||
});
|
||||
|
||||
// Entity-specific filters: Countries - Has holidays
|
||||
this.$dropdown.on('change', '.filter-has-holidays', function() {
|
||||
self.filters.hasHolidays = $(this).is(':checked');
|
||||
self.refreshSearch();
|
||||
});
|
||||
|
||||
// Entity-specific filters: Countries - Contains states
|
||||
this.$dropdown.on('change', '.filter-contains-states', function() {
|
||||
self.filters.containsStates = $(this).is(':checked');
|
||||
self.refreshSearch();
|
||||
});
|
||||
|
||||
// Entity-specific filters: Countries - Zone select
|
||||
this.$dropdown.on('change', '.filter-zone-select', function() {
|
||||
self.filters.zone = $(this).val() || null;
|
||||
self.refreshSearch();
|
||||
});
|
||||
|
||||
// Clear entity-specific filters
|
||||
this.$dropdown.on('click', '.filter-row-entity-categories .btn-clear-filters, .filter-row-entity-manufacturers .btn-clear-filters, .filter-row-entity-suppliers .btn-clear-filters, .filter-row-entity-attributes .btn-clear-filters, .filter-row-entity-features .btn-clear-filters, .filter-row-entity-cms .btn-clear-filters, .filter-row-entity-cms-categories .btn-clear-filters', function(e) {
|
||||
this.$dropdown.on('click', '.filter-row-entity-categories .btn-clear-filters, .filter-row-entity-manufacturers .btn-clear-filters, .filter-row-entity-suppliers .btn-clear-filters, .filter-row-entity-attributes .btn-clear-filters, .filter-row-entity-features .btn-clear-filters, .filter-row-entity-cms .btn-clear-filters, .filter-row-entity-cms-categories .btn-clear-filters, .filter-row-entity-countries .btn-clear-filters', function(e) {
|
||||
e.preventDefault();
|
||||
var $row = $(this).closest('.filter-row');
|
||||
$row.find('input[type="number"]').val('');
|
||||
@@ -1760,6 +1816,10 @@
|
||||
self.filters.dateAddTo = null;
|
||||
self.filters.lastProductFrom = null;
|
||||
self.filters.lastProductTo = null;
|
||||
// Country filters
|
||||
self.filters.hasHolidays = false;
|
||||
self.filters.containsStates = false;
|
||||
self.filters.zone = null;
|
||||
self.refreshSearch();
|
||||
});
|
||||
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
dateAddFrom: null,
|
||||
dateAddTo: null,
|
||||
lastProductFrom: null,
|
||||
lastProductTo: null
|
||||
lastProductTo: null,
|
||||
// Country-specific filters
|
||||
hasHolidays: false,
|
||||
containsStates: false,
|
||||
zone: null
|
||||
};
|
||||
|
||||
if (this.$dropdown) {
|
||||
@@ -66,6 +70,10 @@
|
||||
this.$dropdown.find('.filter-depth-select').val('');
|
||||
this.$dropdown.find('.filter-has-products').prop('checked', false);
|
||||
this.$dropdown.find('.filter-active-only').prop('checked', true);
|
||||
// Country filters
|
||||
this.$dropdown.find('.filter-has-holidays').prop('checked', false);
|
||||
this.$dropdown.find('.filter-contains-states').prop('checked', false);
|
||||
this.$dropdown.find('.filter-zone-select').val('');
|
||||
}
|
||||
|
||||
this.refreshSearch();
|
||||
@@ -99,7 +107,11 @@
|
||||
dateAddFrom: null,
|
||||
dateAddTo: null,
|
||||
lastProductFrom: null,
|
||||
lastProductTo: null
|
||||
lastProductTo: null,
|
||||
// Country-specific filters
|
||||
hasHolidays: false,
|
||||
containsStates: false,
|
||||
zone: null
|
||||
};
|
||||
},
|
||||
|
||||
@@ -133,6 +145,11 @@
|
||||
this.$dropdown.removeClass('view-tree view-cols-2 view-cols-3 view-cols-4 view-cols-5 view-cols-6 view-cols-7 view-cols-8').addClass('view-list');
|
||||
}
|
||||
|
||||
// Load zones for countries filter
|
||||
if (entityType === 'countries') {
|
||||
this.loadZonesForCountryFilter();
|
||||
}
|
||||
|
||||
// Update sort options for entity type
|
||||
this.updateSortOptionsForEntity(entityType);
|
||||
},
|
||||
@@ -334,6 +351,46 @@
|
||||
$toggle.toggleClass('has-selection', hasActiveInGroup);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load zones for country filter dropdown
|
||||
*/
|
||||
loadZonesForCountryFilter: function() {
|
||||
var self = this;
|
||||
|
||||
if (this.zonesLoaded || !this.$dropdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $select = this.$dropdown.find('.filter-zone-select');
|
||||
if (!$select.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'getZonesForFilter',
|
||||
trait: 'EntitySelector'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.zones && response.zones.length > 0) {
|
||||
var trans = self.config.trans || {};
|
||||
$select.empty();
|
||||
$select.append('<option value="">' + (trans.all_zones || 'All zones') + '</option>');
|
||||
|
||||
response.zones.forEach(function(zone) {
|
||||
$select.append('<option value="' + zone.id + '">' + self.escapeHtml(zone.name) + '</option>');
|
||||
});
|
||||
|
||||
self.zonesLoaded = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -552,6 +552,30 @@
|
||||
|
||||
this.previewBlockType = blockType;
|
||||
|
||||
// If items not loaded yet, fetch them first
|
||||
if (items.length === 0 && previewData.count > 0) {
|
||||
$badge.addClass('loading');
|
||||
this.fetchTabPreviewItems($tab, function(fetchedItems, hasMore) {
|
||||
$badge.removeClass('loading');
|
||||
self.createPreviewPopover({
|
||||
$badge: $badge,
|
||||
items: fetchedItems,
|
||||
totalCount: previewData.count,
|
||||
hasMore: hasMore,
|
||||
entityLabel: entityLabelPlural,
|
||||
previewType: 'tab',
|
||||
context: { $tab: $tab, blockType: blockType },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreTabPreviewItems($tab, $btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterTabPreviewItems($tab, query);
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.createPreviewPopover({
|
||||
$badge: $badge,
|
||||
items: items,
|
||||
@@ -569,6 +593,57 @@
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch preview items for a tab via AJAX
|
||||
*/
|
||||
fetchTabPreviewItems: function($tab, callback) {
|
||||
var self = this;
|
||||
var blockType = $tab.data('blockType');
|
||||
|
||||
var $hiddenInput = this.$wrapper.find('input[name="' + this.config.name + '"]');
|
||||
var savedData = {};
|
||||
try {
|
||||
savedData = JSON.parse($hiddenInput.val() || '{}');
|
||||
} catch (e) {
|
||||
callback([], false);
|
||||
return;
|
||||
}
|
||||
|
||||
var groups = (savedData[blockType] && savedData[blockType].groups) ? savedData[blockType].groups : [];
|
||||
if (groups.length === 0) {
|
||||
callback([], false);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {};
|
||||
data[blockType] = { groups: groups };
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'previewEntitySelector',
|
||||
trait: 'EntitySelector',
|
||||
conditions: JSON.stringify(data),
|
||||
block_type: blockType,
|
||||
limit: 20,
|
||||
offset: 0
|
||||
},
|
||||
success: function(response) {
|
||||
var items = response.items || response.products || [];
|
||||
var hasMore = response.hasMore || (response.count > items.length);
|
||||
// Update stored preview data with items
|
||||
$tab.data('previewData', response);
|
||||
callback(items, hasMore);
|
||||
},
|
||||
error: function() {
|
||||
callback([], false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* AJAX filter handler for tab preview
|
||||
*/
|
||||
@@ -686,6 +761,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a country holidays badge
|
||||
if (conditionData.isCountryHolidays && conditionData.countryIds) {
|
||||
this.showCountriesHolidayPreview($badge, conditionData.countryIds);
|
||||
return;
|
||||
}
|
||||
|
||||
this.hidePreviewPopover();
|
||||
|
||||
$badge.addClass('popover-open loading');
|
||||
@@ -1460,6 +1541,392 @@
|
||||
}
|
||||
|
||||
$popover.hide().fadeIn(150);
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// HOLIDAY PREVIEW (Country chip eye button click)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Show holiday preview popover for a country
|
||||
* @param {number} countryId - Country ID
|
||||
* @param {string} countryName - Country name
|
||||
* @param {string} countryIso - Country ISO code
|
||||
* @param {jQuery} $trigger - The button element that triggered this
|
||||
*/
|
||||
showHolidayPreview: function(countryId, countryName, countryIso, $trigger) {
|
||||
var self = this;
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
// Close any existing holiday popover
|
||||
$('.holiday-preview-popover').remove();
|
||||
|
||||
// Create popover HTML
|
||||
var popoverHtml = '<div class="holiday-preview-popover target-preview-popover show">';
|
||||
popoverHtml += '<div class="popover-header">';
|
||||
popoverHtml += '<span class="popover-title">';
|
||||
if (countryIso) {
|
||||
popoverHtml += '<img src="https://flagcdn.com/20x15/' + this.escapeAttr(countryIso.toLowerCase()) + '.png" alt="" class="popover-flag"> ';
|
||||
}
|
||||
popoverHtml += this.escapeHtml(countryName) + ' - ' + (trans.holidays || 'Holidays');
|
||||
popoverHtml += '</span>';
|
||||
popoverHtml += '<button type="button" class="popover-close"><i class="material-icons">close</i></button>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '<div class="popover-body">';
|
||||
popoverHtml += '<div class="holiday-preview-loading"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</div>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '</div>';
|
||||
|
||||
var $popover = $(popoverHtml);
|
||||
$('body').append($popover);
|
||||
|
||||
// Position popover near the trigger button
|
||||
var triggerRect = $trigger[0].getBoundingClientRect();
|
||||
var scrollTop = $(window).scrollTop();
|
||||
var scrollLeft = $(window).scrollLeft();
|
||||
var popoverWidth = $popover.outerWidth();
|
||||
var popoverHeight = $popover.outerHeight();
|
||||
var windowWidth = $(window).width();
|
||||
var windowHeight = $(window).height();
|
||||
|
||||
// Default: position below and to the right of the trigger
|
||||
var top = triggerRect.bottom + scrollTop + 8;
|
||||
var left = triggerRect.left + scrollLeft;
|
||||
|
||||
// Adjust horizontal position if it goes off-screen
|
||||
if (left + popoverWidth > windowWidth - 10) {
|
||||
left = windowWidth - popoverWidth - 10;
|
||||
}
|
||||
if (left < 10) {
|
||||
left = 10;
|
||||
}
|
||||
|
||||
// Adjust vertical position if it goes below viewport
|
||||
if (triggerRect.bottom + popoverHeight > windowHeight - 10) {
|
||||
// Position above the trigger instead
|
||||
top = triggerRect.top + scrollTop - popoverHeight - 8;
|
||||
}
|
||||
|
||||
$popover.css({
|
||||
position: 'absolute',
|
||||
top: top,
|
||||
left: left,
|
||||
zIndex: 10001
|
||||
});
|
||||
|
||||
// Close button handler
|
||||
$popover.find('.popover-close').on('click', function() {
|
||||
$popover.remove();
|
||||
});
|
||||
|
||||
// Fetch holidays via AJAX
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'getHolidaysPreview',
|
||||
trait: 'EntitySelector',
|
||||
id_country: countryId
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.holidays && response.holidays.length > 0) {
|
||||
var listHtml = '<div class="holiday-list">';
|
||||
for (var i = 0; i < response.holidays.length; i++) {
|
||||
var h = response.holidays[i];
|
||||
var typeClass = h.type ? 'holiday-type-' + h.type.toLowerCase().replace(/\s+/g, '-') : '';
|
||||
listHtml += '<div class="holiday-item ' + typeClass + '">';
|
||||
listHtml += '<div class="holiday-date">';
|
||||
listHtml += '<span class="holiday-day">' + self.escapeHtml(h.date_formatted || h.date) + '</span>';
|
||||
if (h.day_of_week) {
|
||||
listHtml += '<span class="holiday-weekday">' + self.escapeHtml(h.day_of_week) + '</span>';
|
||||
}
|
||||
listHtml += '</div>';
|
||||
listHtml += '<div class="holiday-info">';
|
||||
listHtml += '<span class="holiday-name">' + self.escapeHtml(h.name) + '</span>';
|
||||
if (h.type) {
|
||||
listHtml += '<span class="holiday-type-badge">' + self.escapeHtml(h.type) + '</span>';
|
||||
}
|
||||
listHtml += '</div>';
|
||||
listHtml += '</div>';
|
||||
}
|
||||
listHtml += '</div>';
|
||||
|
||||
if (response.total_count) {
|
||||
listHtml += '<div class="holiday-preview-note">' + response.total_count + ' ' + (trans.upcoming_holidays || 'upcoming holidays') + '</div>';
|
||||
}
|
||||
|
||||
$popover.find('.popover-body').html(listHtml);
|
||||
} else {
|
||||
var noDataHtml = '<div class="holiday-preview-empty">';
|
||||
noDataHtml += '<i class="material-icons">event_busy</i>';
|
||||
noDataHtml += '<p>' + (trans.no_holidays || 'No holidays found') + '</p>';
|
||||
noDataHtml += '</div>';
|
||||
$popover.find('.popover-body').html(noDataHtml);
|
||||
}
|
||||
|
||||
// Re-adjust position after content loaded
|
||||
var newPopoverHeight = $popover.outerHeight();
|
||||
if (triggerRect.bottom + newPopoverHeight > windowHeight - 10) {
|
||||
var newTop = triggerRect.top + scrollTop - newPopoverHeight - 8;
|
||||
if (newTop > 10) {
|
||||
$popover.css('top', newTop);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
var errorHtml = '<div class="holiday-preview-empty">';
|
||||
errorHtml += '<i class="material-icons">error_outline</i>';
|
||||
errorHtml += '<p>' + (trans.error_loading || 'Error loading holidays') + '</p>';
|
||||
errorHtml += '</div>';
|
||||
$popover.find('.popover-body').html(errorHtml);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// COUNTRIES HOLIDAY PREVIEW (Condition badge for multiple countries)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Show holiday preview popover for multiple selected countries
|
||||
* @param {jQuery} $badge - The condition-match-count badge element
|
||||
* @param {Array} countryIds - Array of country IDs
|
||||
*/
|
||||
showCountriesHolidayPreview: function($badge, countryIds) {
|
||||
var self = this;
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
// Close any existing popovers
|
||||
this.hidePreviewPopover();
|
||||
$('.holiday-preview-popover').remove();
|
||||
|
||||
// Create popover HTML with placeholder title (will update after AJAX)
|
||||
var popoverHtml = '<div class="holiday-preview-popover target-preview-popover show">';
|
||||
popoverHtml += '<div class="popover-header">';
|
||||
popoverHtml += '<span class="popover-title"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</span>';
|
||||
popoverHtml += '<button type="button" class="popover-close"><i class="material-icons">close</i></button>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '<div class="popover-filter">';
|
||||
popoverHtml += '<i class="material-icons">search</i>';
|
||||
popoverHtml += '<input type="text" class="holiday-filter-input" placeholder="' + (trans.filter_holidays || 'Filter by country, date, name...') + '">';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '<div class="popover-body">';
|
||||
popoverHtml += '<div class="holiday-preview-loading"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</div>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '</div>';
|
||||
|
||||
var $popover = $(popoverHtml);
|
||||
$('body').append($popover);
|
||||
|
||||
// Position popover near the badge
|
||||
var badgeRect = $badge[0].getBoundingClientRect();
|
||||
var scrollTop = $(window).scrollTop();
|
||||
var scrollLeft = $(window).scrollLeft();
|
||||
var popoverWidth = $popover.outerWidth();
|
||||
var popoverHeight = $popover.outerHeight();
|
||||
var windowWidth = $(window).width();
|
||||
var windowHeight = $(window).height();
|
||||
|
||||
// Default: position below the badge
|
||||
var top = badgeRect.bottom + scrollTop + 8;
|
||||
var left = badgeRect.left + scrollLeft;
|
||||
|
||||
// Adjust horizontal position
|
||||
if (left + popoverWidth > windowWidth - 10) {
|
||||
left = windowWidth - popoverWidth - 10;
|
||||
}
|
||||
if (left < 10) {
|
||||
left = 10;
|
||||
}
|
||||
|
||||
// Adjust vertical position if it goes below viewport
|
||||
if (badgeRect.bottom + popoverHeight > windowHeight - 10) {
|
||||
top = badgeRect.top + scrollTop - popoverHeight - 8;
|
||||
}
|
||||
|
||||
$popover.css({
|
||||
position: 'absolute',
|
||||
top: top,
|
||||
left: left,
|
||||
zIndex: 10001
|
||||
});
|
||||
|
||||
// Mark badge as open
|
||||
$badge.addClass('popover-open');
|
||||
this.$activeBadge = $badge;
|
||||
|
||||
// Close button handler
|
||||
$popover.find('.popover-close').on('click', function() {
|
||||
$popover.remove();
|
||||
$badge.removeClass('popover-open');
|
||||
self.$activeBadge = null;
|
||||
});
|
||||
|
||||
// Fetch holidays via AJAX
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'getHolidaysForCountries',
|
||||
trait: 'EntitySelector',
|
||||
country_ids: countryIds.join(','),
|
||||
count_only: 0
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.holidays && response.holidays.length > 0) {
|
||||
// Count holidays per country to determine which flags to show
|
||||
var countryHolidayCounts = {};
|
||||
var countriesMap = {};
|
||||
|
||||
// Build map of countries from response
|
||||
if (response.countries) {
|
||||
for (var ci = 0; ci < response.countries.length; ci++) {
|
||||
var cInfo = response.countries[ci];
|
||||
countriesMap[cInfo.id] = cInfo;
|
||||
countryHolidayCounts[cInfo.id] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Count holidays per country
|
||||
for (var hi = 0; hi < response.holidays.length; hi++) {
|
||||
var hol = response.holidays[hi];
|
||||
if (hol.country_id && typeof countryHolidayCounts[hol.country_id] !== 'undefined') {
|
||||
countryHolidayCounts[hol.country_id]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort countries by holiday count (descending)
|
||||
var sortedCountries = Object.keys(countriesMap).sort(function(a, b) {
|
||||
return (countryHolidayCounts[b] || 0) - (countryHolidayCounts[a] || 0);
|
||||
}).map(function(id) {
|
||||
return countriesMap[id];
|
||||
});
|
||||
|
||||
// Build header title with flags for countries with most holidays
|
||||
var titleHtml = '';
|
||||
var numCountries = sortedCountries.length;
|
||||
if (numCountries <= 3) {
|
||||
for (var fi = 0; fi < numCountries; fi++) {
|
||||
var fc = sortedCountries[fi];
|
||||
if (fc && fc.iso_code) {
|
||||
titleHtml += '<img src="https://flagcdn.com/20x15/' + self.escapeAttr(fc.iso_code.toLowerCase()) + '.png" alt="' + self.escapeAttr(fc.iso_code) + '" class="popover-flag" title="' + self.escapeAttr(fc.name) + '"> ';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show top 2 flags (countries with most holidays) + count
|
||||
for (var fj = 0; fj < 2; fj++) {
|
||||
var fcc = sortedCountries[fj];
|
||||
if (fcc && fcc.iso_code) {
|
||||
titleHtml += '<img src="https://flagcdn.com/20x15/' + self.escapeAttr(fcc.iso_code.toLowerCase()) + '.png" alt="" class="popover-flag" title="' + self.escapeAttr(fcc.name) + '"> ';
|
||||
}
|
||||
}
|
||||
titleHtml += '+' + (numCountries - 2) + ' ';
|
||||
}
|
||||
titleHtml += response.total_count + ' ' + (trans.holidays || 'Holidays');
|
||||
|
||||
// Update header title
|
||||
$popover.find('.popover-title').html(titleHtml);
|
||||
|
||||
var listHtml = '<div class="holiday-list">';
|
||||
for (var i = 0; i < response.holidays.length; i++) {
|
||||
var h = response.holidays[i];
|
||||
var typeClass = h.type ? 'holiday-type-' + h.type.toLowerCase().replace(/\s+/g, '-') : '';
|
||||
// Build search text for filtering
|
||||
var searchText = [
|
||||
h.name || '',
|
||||
h.date_formatted || h.date || '',
|
||||
h.day_of_week || '',
|
||||
h.country_name || '',
|
||||
h.type || ''
|
||||
].join(' ').toLowerCase();
|
||||
listHtml += '<div class="holiday-item ' + typeClass + '" data-search="' + self.escapeAttr(searchText) + '">';
|
||||
listHtml += '<div class="holiday-date">';
|
||||
listHtml += '<span class="holiday-day">' + self.escapeHtml(h.date_formatted || h.date) + '</span>';
|
||||
if (h.day_of_week) {
|
||||
listHtml += '<span class="holiday-weekday">' + self.escapeHtml(h.day_of_week) + '</span>';
|
||||
}
|
||||
listHtml += '</div>';
|
||||
listHtml += '<div class="holiday-info">';
|
||||
// Show country flag before holiday name when multiple countries
|
||||
if (h.country_iso && countryIds.length > 1) {
|
||||
listHtml += '<img src="https://flagcdn.com/16x12/' + self.escapeAttr(h.country_iso.toLowerCase()) + '.png" alt="" class="holiday-country-flag" title="' + self.escapeAttr(h.country_name || '') + '"> ';
|
||||
}
|
||||
listHtml += '<span class="holiday-name">' + self.escapeHtml(h.name) + '</span>';
|
||||
if (h.type) {
|
||||
listHtml += '<span class="holiday-type-badge">' + self.escapeHtml(h.type) + '</span>';
|
||||
}
|
||||
listHtml += '</div>';
|
||||
listHtml += '</div>';
|
||||
}
|
||||
listHtml += '</div>';
|
||||
|
||||
if (response.total_count && countryIds.length > 1) {
|
||||
var noteText = (trans.across_countries || 'across') + ' ' + countryIds.length + ' ' + (trans.countries || 'countries');
|
||||
listHtml += '<div class="holiday-preview-note">' + noteText + '</div>';
|
||||
}
|
||||
|
||||
$popover.find('.popover-body').html(listHtml);
|
||||
|
||||
// Setup filter input handler
|
||||
$popover.find('.holiday-filter-input').on('input', function() {
|
||||
var query = $(this).val().toLowerCase().trim();
|
||||
var $items = $popover.find('.holiday-item');
|
||||
var visibleCount = 0;
|
||||
|
||||
$items.each(function() {
|
||||
var searchData = $(this).attr('data-search') || '';
|
||||
if (!query || searchData.indexOf(query) !== -1) {
|
||||
$(this).show();
|
||||
visibleCount++;
|
||||
} else {
|
||||
$(this).hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Update note with filtered count
|
||||
var $note = $popover.find('.holiday-preview-note');
|
||||
if (query && $note.length) {
|
||||
$note.text(visibleCount + ' ' + (trans.matches || 'matches'));
|
||||
} else if ($note.length && countryIds.length > 1) {
|
||||
var noteText = (trans.across_countries || 'across') + ' ' + countryIds.length + ' ' + (trans.countries || 'countries');
|
||||
$note.text(noteText);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Update header for empty state
|
||||
$popover.find('.popover-title').html('0 ' + (trans.holidays || 'Holidays'));
|
||||
|
||||
var noDataHtml = '<div class="holiday-preview-empty">';
|
||||
noDataHtml += '<i class="material-icons">event_busy</i>';
|
||||
noDataHtml += '<p>' + (trans.no_holidays || 'No holidays found') + '</p>';
|
||||
noDataHtml += '</div>';
|
||||
$popover.find('.popover-body').html(noDataHtml);
|
||||
}
|
||||
|
||||
// Re-adjust position after content loaded
|
||||
var newPopoverHeight = $popover.outerHeight();
|
||||
if (badgeRect.bottom + newPopoverHeight > windowHeight - 10) {
|
||||
var newTop = badgeRect.top + scrollTop - newPopoverHeight - 8;
|
||||
if (newTop > 10) {
|
||||
$popover.css('top', newTop);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
// Update header for error state
|
||||
$popover.find('.popover-title').html('<i class="material-icons">error_outline</i> ' + (trans.error || 'Error'));
|
||||
|
||||
var errorHtml = '<div class="holiday-preview-empty">';
|
||||
errorHtml += '<i class="material-icons">error_outline</i>';
|
||||
errorHtml += '<p>' + (trans.error_loading || 'Error loading holidays') + '</p>';
|
||||
errorHtml += '</div>';
|
||||
$popover.find('.popover-body').html(errorHtml);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Reusable patterns - prefer Bootstrap utilities in HTML where possible
|
||||
*/
|
||||
|
||||
@use "sass:color";
|
||||
@use 'variables' as *;
|
||||
|
||||
// =============================================================================
|
||||
@@ -231,7 +232,7 @@
|
||||
|
||||
// Popover open state
|
||||
&.popover-open {
|
||||
background: darken($bg, 10%);
|
||||
background: color.adjust($bg, $lightness: -10%);
|
||||
box-shadow: 0 2px 8px rgba($bg, 0.4);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Product attribute combination selection styles
|
||||
*/
|
||||
|
||||
@use "sass:color";
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
@@ -298,7 +299,7 @@
|
||||
|
||||
&:hover {
|
||||
background: $es-primary-hover;
|
||||
border-color: darken($es-primary-hover, 5%);
|
||||
border-color: color.adjust($es-primary-hover, $lightness: -5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,29 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// Schedule summary (shows current config at a glance)
|
||||
.trait-summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-medium;
|
||||
color: $es-primary;
|
||||
background: rgba($es-primary, 0.08);
|
||||
border-radius: $es-radius-full;
|
||||
white-space: nowrap;
|
||||
margin-left: $es-spacing-md;
|
||||
flex-shrink: 0;
|
||||
max-width: 320px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.trait-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -277,6 +300,48 @@
|
||||
gap: $es-spacing-md;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Collapse Header (form-content layout)
|
||||
// =============================================================================
|
||||
|
||||
.entity-selector-collapse-header {
|
||||
padding: 0;
|
||||
margin-bottom: $es-spacing-sm;
|
||||
|
||||
.btn-collapse-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: $es-primary;
|
||||
font-size: $es-font-size-sm;
|
||||
cursor: pointer;
|
||||
transition: color $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
color: $es-primary-hover;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
font-size: 1.25rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.collapse-label {
|
||||
font-weight: $es-font-weight-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When collapsed, rotate icon
|
||||
.condition-trait.collapsed .entity-selector-collapse-header {
|
||||
.collapse-icon {
|
||||
// Icon already shows expand_more when collapsed
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Animations
|
||||
// =============================================================================
|
||||
|
||||
@@ -229,6 +229,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs row with actions (form-content layout)
|
||||
.entity-selector-tabs-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: $es-slate-100;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-lg $es-radius-lg 0 0;
|
||||
|
||||
.target-block-tabs {
|
||||
flex: 1;
|
||||
border-bottom: 0;
|
||||
border-radius: $es-radius-lg 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Expand/collapse toggle area (entire section is clickable)
|
||||
.entity-selector-actions.btn-toggle-blocks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 $es-spacing-md;
|
||||
background: $es-slate-100;
|
||||
border-left: 1px solid $es-border-color;
|
||||
color: $es-slate-400;
|
||||
cursor: pointer;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-200;
|
||||
color: $es-primary;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// When expanded - highlight the toggle area
|
||||
.entity-selector-trait:not(.blocks-collapsed) .entity-selector-actions.btn-toggle-blocks {
|
||||
background: $es-primary-light;
|
||||
border-left-color: $es-primary;
|
||||
color: $es-primary;
|
||||
}
|
||||
|
||||
// Blocks content wrapper (for form-content layout collapse)
|
||||
.entity-selector-blocks-content {
|
||||
// Inherits styles from condition-trait-body context
|
||||
}
|
||||
|
||||
// Block container
|
||||
.target-block-container {
|
||||
display: none;
|
||||
|
||||
@@ -954,6 +954,20 @@
|
||||
color: $es-primary;
|
||||
}
|
||||
|
||||
// Country holidays variant - use calendar icon style
|
||||
&.country-holidays {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
color: #8b5cf6;
|
||||
|
||||
&:hover {
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
&.clickable {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@@ -517,3 +517,73 @@
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Schedule Preview Dropdown (for admin list columns)
|
||||
// =============================================================================
|
||||
|
||||
.schedule-preview-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 4px;
|
||||
z-index: 1000;
|
||||
min-width: 180px;
|
||||
padding: $es-spacing-sm;
|
||||
background: $es-white;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-md;
|
||||
box-shadow: $es-shadow-lg;
|
||||
font-size: $es-font-size-sm;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.schedule-preview-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-xs 0;
|
||||
color: $es-text-primary;
|
||||
|
||||
.material-icons {
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
color: $es-text-muted;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $es-font-weight-semibold;
|
||||
}
|
||||
|
||||
.schedule-time {
|
||||
color: $es-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
// Badge trigger for schedule preview
|
||||
.btn-schedule-preview {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 0.5rem;
|
||||
background: $es-primary;
|
||||
color: $es-white;
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
border-radius: 50rem;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
|
||||
.icon-eye {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Preview modals, confirmation dialogs
|
||||
*/
|
||||
|
||||
@use "sass:color";
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
@@ -162,7 +163,7 @@
|
||||
background: $es-danger;
|
||||
|
||||
&:hover {
|
||||
background: darken($es-danger, 10%);
|
||||
background: color.adjust($es-danger, $lightness: -10%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,3 +274,215 @@
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Holiday Preview Modal
|
||||
// ==========================================================================
|
||||
|
||||
#mpr-holiday-preview-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $es-z-modal;
|
||||
|
||||
&.show {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mpr-modal-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mpr-modal-dialog {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
max-height: 80vh;
|
||||
background: $es-white;
|
||||
border-radius: $es-radius-lg;
|
||||
box-shadow: $es-shadow-xl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mpr-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $es-spacing-md;
|
||||
padding: $es-spacing-md $es-spacing-lg;
|
||||
background: $es-bg-header;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mpr-modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
font-size: $es-font-size-base;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
color: $es-text-primary;
|
||||
margin: 0;
|
||||
|
||||
i.material-icons {
|
||||
font-size: 20px;
|
||||
color: $es-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.mpr-modal-close {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: $es-text-muted;
|
||||
border-radius: $es-radius-md;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-200;
|
||||
color: $es-text-secondary;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.mpr-modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $es-spacing-lg;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
// Loading state
|
||||
.holiday-preview-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-xl 0;
|
||||
color: $es-text-muted;
|
||||
font-size: $es-font-size-sm;
|
||||
|
||||
i {
|
||||
font-size: $es-font-size-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
.holiday-preview-empty {
|
||||
text-align: center;
|
||||
padding: $es-spacing-xl 0;
|
||||
color: $es-text-muted;
|
||||
|
||||
i.material-icons {
|
||||
font-size: 48px;
|
||||
opacity: 0.5;
|
||||
margin-bottom: $es-spacing-md;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $es-spacing-xs;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: $es-font-size-xs;
|
||||
color: $es-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
// Holiday list
|
||||
.holiday-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $es-spacing-sm;
|
||||
}
|
||||
|
||||
.holiday-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $es-spacing-md;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
background: $es-slate-50;
|
||||
border-radius: $es-radius-md;
|
||||
border-left: 3px solid $es-success;
|
||||
|
||||
&.holiday-type-bank {
|
||||
border-left-color: $es-info;
|
||||
}
|
||||
|
||||
&.holiday-type-observance {
|
||||
border-left-color: $es-warning;
|
||||
}
|
||||
|
||||
&.holiday-type-regional {
|
||||
border-left-color: #8b5cf6;
|
||||
}
|
||||
}
|
||||
|
||||
.holiday-date {
|
||||
flex-shrink: 0;
|
||||
min-width: 100px;
|
||||
|
||||
.holiday-day {
|
||||
display: block;
|
||||
font-size: $es-font-size-sm;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
color: $es-text-primary;
|
||||
}
|
||||
|
||||
.holiday-weekday {
|
||||
display: block;
|
||||
font-size: $es-font-size-xs;
|
||||
color: $es-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.holiday-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.holiday-name {
|
||||
display: block;
|
||||
font-size: $es-font-size-sm;
|
||||
color: $es-text-primary;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.holiday-type-badge {
|
||||
display: inline-block;
|
||||
margin-top: $es-spacing-xs;
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 10px;
|
||||
font-weight: $es-font-weight-medium;
|
||||
text-transform: capitalize;
|
||||
background: $es-slate-200;
|
||||
color: $es-text-secondary;
|
||||
border-radius: $es-radius-sm;
|
||||
}
|
||||
|
||||
.holiday-preview-note {
|
||||
margin-top: $es-spacing-md;
|
||||
font-size: $es-font-size-xs;
|
||||
color: $es-text-muted;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Hierarchical tree view for category selection inside dropdown
|
||||
*/
|
||||
|
||||
@use "sass:color";
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
@@ -231,7 +232,7 @@
|
||||
}
|
||||
|
||||
&.popover-open {
|
||||
background: darken($es-primary, 10%);
|
||||
background: color.adjust($es-primary, $lightness: -10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,28 +201,36 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
color: $es-text-secondary;
|
||||
background: $es-slate-100;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $es-radius-sm;
|
||||
padding: 0.375rem 0.75rem;
|
||||
color: $es-text-muted;
|
||||
background: transparent;
|
||||
border: 1px dashed $es-border-color;
|
||||
border-radius: 100px; // Pill shape
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-medium;
|
||||
font-weight: $es-font-weight-normal;
|
||||
cursor: pointer;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-200;
|
||||
color: $es-text-secondary;
|
||||
border-color: $es-slate-400;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: $es-primary;
|
||||
background: $es-primary-light;
|
||||
border-color: $es-primary;
|
||||
border: 1px solid $es-primary;
|
||||
font-weight: $es-font-weight-medium;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.selected i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -281,6 +281,15 @@ trait EntitySelector
|
||||
case 'previewCategoryPages':
|
||||
$this->ajaxPreviewCategoryPages();
|
||||
return true;
|
||||
case 'getHolidaysPreview':
|
||||
$this->ajaxGetHolidaysPreview();
|
||||
return true;
|
||||
case 'getHolidaysForCountries':
|
||||
$this->ajaxGetHolidaysForCountries();
|
||||
return true;
|
||||
case 'getZonesForFilter':
|
||||
$this->ajaxGetZonesForFilter();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -652,6 +661,40 @@ trait EntitySelector
|
||||
|
||||
$finalCount = count($matchingIds);
|
||||
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => true,
|
||||
'include_count' => $includeCount,
|
||||
'exclude_count' => $excludeCount,
|
||||
'final_count' => $finalCount,
|
||||
]));
|
||||
} elseif ($blockType === 'countries') {
|
||||
// For countries, count selected countries (not holidays)
|
||||
$includeMethod = $groupData['include']['method'] ?? 'all';
|
||||
$includeValues = $groupData['include']['values'] ?? [];
|
||||
|
||||
// For 'specific' method, count is the number of selected countries
|
||||
// For 'all', we'd need to count all active countries
|
||||
if ($includeMethod === 'specific') {
|
||||
$includeCount = count($includeValues);
|
||||
} elseif ($includeMethod === 'all') {
|
||||
// Count all active countries
|
||||
$includeCount = (int) Db::getInstance()->getValue(
|
||||
'SELECT COUNT(*) FROM ' . _DB_PREFIX_ . 'country WHERE active = 1'
|
||||
);
|
||||
} else {
|
||||
$includeCount = 0;
|
||||
}
|
||||
|
||||
$excludeCount = 0;
|
||||
if (!empty($groupData['excludes']) && is_array($groupData['excludes'])) {
|
||||
foreach ($groupData['excludes'] as $exclude) {
|
||||
$excludeValues = $exclude['values'] ?? [];
|
||||
$excludeCount += count($excludeValues);
|
||||
}
|
||||
}
|
||||
|
||||
$finalCount = max(0, $includeCount - $excludeCount);
|
||||
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => true,
|
||||
'include_count' => $includeCount,
|
||||
@@ -1193,6 +1236,11 @@ trait EntitySelector
|
||||
$filterIsCustom = (bool) Tools::getValue('filter_is_custom', 0);
|
||||
$filterIndexable = (bool) Tools::getValue('filter_indexable', 0);
|
||||
|
||||
// Country-specific filters
|
||||
$filterHasHolidays = (bool) Tools::getValue('filter_has_holidays', 0);
|
||||
$filterContainsStates = (bool) Tools::getValue('filter_contains_states', 0);
|
||||
$filterZone = Tools::getValue('filter_zone', '');
|
||||
|
||||
$filters = [
|
||||
'attributes' => $filterAttributes ? json_decode($filterAttributes, true) : [],
|
||||
'features' => $filterFeatures ? json_decode($filterFeatures, true) : [],
|
||||
@@ -1224,6 +1272,10 @@ trait EntitySelector
|
||||
'is_color' => $filterIsColor,
|
||||
'is_custom' => $filterIsCustom,
|
||||
'indexable' => $filterIndexable,
|
||||
// Country-specific
|
||||
'has_holidays' => $filterHasHolidays,
|
||||
'contains_states' => $filterContainsStates,
|
||||
'zone' => $filterZone !== '' ? (int) $filterZone : null,
|
||||
// Sorting
|
||||
'sort_by' => $sortBy,
|
||||
'sort_dir' => $sortDir,
|
||||
@@ -1275,6 +1327,8 @@ trait EntitySelector
|
||||
protected function ajaxGetTargetEntitiesByIdsBulk()
|
||||
{
|
||||
$entitiesParam = Tools::getValue('entities', '');
|
||||
\PrestaShopLogger::addLog('[EntitySelector] ajaxGetTargetEntitiesByIdsBulk called, raw param: ' . substr($entitiesParam, 0, 500), 1);
|
||||
|
||||
if (is_string($entitiesParam)) {
|
||||
$entitiesParam = json_decode($entitiesParam, true);
|
||||
}
|
||||
@@ -1282,16 +1336,21 @@ trait EntitySelector
|
||||
$entitiesParam = [];
|
||||
}
|
||||
|
||||
\PrestaShopLogger::addLog('[EntitySelector] Parsed entities param: ' . json_encode($entitiesParam), 1);
|
||||
|
||||
$result = [];
|
||||
$searchEngine = $this->getEntitySearchEngine();
|
||||
|
||||
foreach ($entitiesParam as $entityType => $ids) {
|
||||
if (!is_array($ids) || empty($ids)) {
|
||||
\PrestaShopLogger::addLog('[EntitySelector] Skipping entityType=' . $entityType . ' - empty or not array', 1);
|
||||
continue;
|
||||
}
|
||||
// Deduplicate and sanitize IDs
|
||||
$ids = array_unique(array_map('intval', $ids));
|
||||
\PrestaShopLogger::addLog('[EntitySelector] Fetching entityType=' . $entityType . ' ids=' . implode(',', $ids), 1);
|
||||
$result[$entityType] = $searchEngine->getByIds($entityType, $ids);
|
||||
\PrestaShopLogger::addLog('[EntitySelector] Got ' . count($result[$entityType]) . ' results for ' . $entityType, 1);
|
||||
}
|
||||
|
||||
$this->ajaxDie(json_encode([
|
||||
@@ -3605,4 +3664,248 @@ trait EntitySelector
|
||||
'groups' => $groups ?: [],
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get holidays preview for a country
|
||||
*/
|
||||
protected function ajaxGetHolidaysPreview()
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$idCountry = (int) Tools::getValue('id_country');
|
||||
|
||||
if (!$idCountry) {
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Country ID is required.',
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if PublicHoliday class exists
|
||||
if (!class_exists('MyPrestaRocks\\PublicHolidays\\PublicHoliday')) {
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Holiday system not available.',
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
$idLang = (int) $this->context->language->id;
|
||||
$currentYear = (int) date('Y');
|
||||
|
||||
// Get country info
|
||||
$country = new \Country($idCountry, $idLang);
|
||||
if (!\Validate::isLoadedObject($country)) {
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Country not found.',
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get holidays for current and next year
|
||||
$holidays = [];
|
||||
try {
|
||||
$holidaysThisYear = \MyPrestaRocks\PublicHolidays\PublicHoliday::getHolidaysForCountry(
|
||||
$idCountry,
|
||||
$currentYear,
|
||||
$idLang
|
||||
);
|
||||
$holidaysNextYear = \MyPrestaRocks\PublicHolidays\PublicHoliday::getHolidaysForCountry(
|
||||
$idCountry,
|
||||
$currentYear + 1,
|
||||
$idLang
|
||||
);
|
||||
|
||||
$allHolidays = array_merge($holidaysThisYear ?: [], $holidaysNextYear ?: []);
|
||||
|
||||
// Filter to show only future holidays and format
|
||||
$today = date('Y-m-d');
|
||||
foreach ($allHolidays as $holiday) {
|
||||
if ($holiday['holiday_date'] >= $today) {
|
||||
$holidays[] = [
|
||||
'date' => $holiday['holiday_date'],
|
||||
'date_formatted' => \Tools::displayDate($holiday['holiday_date'], null, false),
|
||||
'day_of_week' => date('l', strtotime($holiday['holiday_date'])),
|
||||
'name' => $holiday['display_name'] ?? $holiday['name'],
|
||||
'type' => $holiday['type'],
|
||||
'recurring' => (bool) $holiday['recurring'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by date
|
||||
usort($holidays, function ($a, $b) {
|
||||
return strcmp($a['date'], $b['date']);
|
||||
});
|
||||
|
||||
// Limit to next 20 holidays
|
||||
$holidays = array_slice($holidays, 0, 20);
|
||||
} catch (\Exception $e) {
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => true,
|
||||
'country' => [
|
||||
'id' => $idCountry,
|
||||
'name' => $country->name,
|
||||
'iso_code' => $country->iso_code,
|
||||
],
|
||||
'holidays' => $holidays,
|
||||
'total_count' => count($holidays),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get holidays for multiple countries
|
||||
* Used for the condition-match-count badge and combined preview
|
||||
*/
|
||||
protected function ajaxGetHolidaysForCountries()
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$countryIds = Tools::getValue('country_ids');
|
||||
$countOnly = (bool) Tools::getValue('count_only', false);
|
||||
|
||||
// Parse country IDs (can be comma-separated string or array)
|
||||
if (is_string($countryIds)) {
|
||||
$countryIds = array_filter(array_map('intval', explode(',', $countryIds)));
|
||||
} elseif (is_array($countryIds)) {
|
||||
$countryIds = array_filter(array_map('intval', $countryIds));
|
||||
} else {
|
||||
$countryIds = [];
|
||||
}
|
||||
|
||||
if (empty($countryIds)) {
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => true,
|
||||
'holidays' => [],
|
||||
'total_count' => 0,
|
||||
'countries' => [],
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if PublicHoliday class exists
|
||||
if (!class_exists('MyPrestaRocks\\PublicHolidays\\PublicHoliday')) {
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Holiday system not available.',
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
$idLang = (int) $this->context->language->id;
|
||||
$currentYear = (int) date('Y');
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$allHolidays = [];
|
||||
$countriesInfo = [];
|
||||
|
||||
try {
|
||||
foreach ($countryIds as $idCountry) {
|
||||
$country = new \Country($idCountry, $idLang);
|
||||
if (!\Validate::isLoadedObject($country)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$countriesInfo[$idCountry] = [
|
||||
'id' => $idCountry,
|
||||
'name' => $country->name,
|
||||
'iso_code' => $country->iso_code,
|
||||
];
|
||||
|
||||
// Get holidays for current and next year
|
||||
$holidaysThisYear = \MyPrestaRocks\PublicHolidays\PublicHoliday::getHolidaysForCountry(
|
||||
$idCountry,
|
||||
$currentYear,
|
||||
$idLang
|
||||
);
|
||||
$holidaysNextYear = \MyPrestaRocks\PublicHolidays\PublicHoliday::getHolidaysForCountry(
|
||||
$idCountry,
|
||||
$currentYear + 1,
|
||||
$idLang
|
||||
);
|
||||
|
||||
$countryHolidays = array_merge($holidaysThisYear ?: [], $holidaysNextYear ?: []);
|
||||
|
||||
// Filter to show only future holidays
|
||||
foreach ($countryHolidays as $holiday) {
|
||||
if ($holiday['holiday_date'] >= $today) {
|
||||
$allHolidays[] = [
|
||||
'date' => $holiday['holiday_date'],
|
||||
'date_formatted' => \Tools::displayDate($holiday['holiday_date'], null, false),
|
||||
'day_of_week' => date('l', strtotime($holiday['holiday_date'])),
|
||||
'name' => $holiday['display_name'] ?? $holiday['name'],
|
||||
'type' => $holiday['type'],
|
||||
'recurring' => (bool) $holiday['recurring'],
|
||||
'country_id' => $idCountry,
|
||||
'country_name' => $country->name,
|
||||
'country_iso' => $country->iso_code,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by date
|
||||
usort($allHolidays, function ($a, $b) {
|
||||
return strcmp($a['date'], $b['date']);
|
||||
});
|
||||
|
||||
$totalCount = count($allHolidays);
|
||||
|
||||
// If only count requested, return early
|
||||
if ($countOnly) {
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => true,
|
||||
'total_count' => $totalCount,
|
||||
'countries' => array_values($countriesInfo),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit to next 50 holidays for full preview
|
||||
$allHolidays = array_slice($allHolidays, 0, 50);
|
||||
} catch (\Exception $e) {
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => true,
|
||||
'holidays' => $allHolidays,
|
||||
'total_count' => $totalCount,
|
||||
'countries' => array_values($countriesInfo),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get zones for country filter dropdown
|
||||
*/
|
||||
protected function ajaxGetZonesForFilter()
|
||||
{
|
||||
$idLang = (int) $this->context->language->id;
|
||||
|
||||
$zones = \Zone::getZones(true);
|
||||
$result = [];
|
||||
|
||||
foreach ($zones as $zone) {
|
||||
$result[] = [
|
||||
'id' => (int) $zone['id_zone'],
|
||||
'name' => $zone['name'],
|
||||
];
|
||||
}
|
||||
|
||||
$this->ajaxDie(json_encode([
|
||||
'success' => true,
|
||||
'zones' => $result,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2082,9 +2082,8 @@ class EntitySearchEngine
|
||||
/**
|
||||
* Search countries
|
||||
*/
|
||||
public function searchTargetCountries($query, $idLang, $idShop, array $filters = [])
|
||||
public function searchTargetCountries($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
|
||||
{
|
||||
$limit = isset($filters['limit']) ? (int) $filters['limit'] : 20;
|
||||
|
||||
$sql = new DbQuery();
|
||||
$sql->select('DISTINCT c.id_country, cl.name, c.iso_code, c.active, c.id_zone, c.contains_states, c.need_zip_code');
|
||||
@@ -2112,6 +2111,29 @@ class EntitySearchEngine
|
||||
$sql->where('c.active = 1');
|
||||
}
|
||||
|
||||
// Zone filter
|
||||
if (!empty($filters['zone'])) {
|
||||
$sql->where('c.id_zone = ' . (int) $filters['zone']);
|
||||
}
|
||||
|
||||
// Contains states filter
|
||||
if (!empty($filters['contains_states'])) {
|
||||
$sql->where('c.contains_states = 1');
|
||||
}
|
||||
|
||||
// Has holidays filter - check if PublicHoliday class exists and country has holidays
|
||||
if (!empty($filters['has_holidays'])) {
|
||||
if (class_exists('MyPrestaRocks\\PublicHolidays\\PublicHoliday')) {
|
||||
$countriesWithHolidays = \MyPrestaRocks\PublicHolidays\PublicHoliday::getCountriesWithHolidays();
|
||||
if (!empty($countriesWithHolidays)) {
|
||||
$sql->where('c.id_country IN (' . implode(',', array_map('intval', $countriesWithHolidays)) . ')');
|
||||
} else {
|
||||
// No countries with holidays, return empty
|
||||
$sql->where('1 = 0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting
|
||||
$countrySortMap = [
|
||||
'name' => 'cl.name',
|
||||
@@ -2120,7 +2142,7 @@ class EntitySearchEngine
|
||||
'iso_code' => 'c.iso_code',
|
||||
];
|
||||
$sql->orderBy($this->buildOrderBy('countries', $filters, $countrySortMap));
|
||||
$sql->limit($limit);
|
||||
$sql->limit($limit, $offset);
|
||||
|
||||
$results = Db::getInstance()->executeS($sql);
|
||||
|
||||
@@ -2171,6 +2193,28 @@ class EntitySearchEngine
|
||||
$sql->where('c.active = 1');
|
||||
}
|
||||
|
||||
// Zone filter
|
||||
if (!empty($filters['zone'])) {
|
||||
$sql->where('c.id_zone = ' . (int) $filters['zone']);
|
||||
}
|
||||
|
||||
// Contains states filter
|
||||
if (!empty($filters['contains_states'])) {
|
||||
$sql->where('c.contains_states = 1');
|
||||
}
|
||||
|
||||
// Has holidays filter
|
||||
if (!empty($filters['has_holidays'])) {
|
||||
if (class_exists('MyPrestaRocks\\PublicHolidays\\PublicHoliday')) {
|
||||
$countriesWithHolidays = \MyPrestaRocks\PublicHolidays\PublicHoliday::getCountriesWithHolidays();
|
||||
if (!empty($countriesWithHolidays)) {
|
||||
$sql->where('c.id_country IN (' . implode(',', array_map('intval', $countriesWithHolidays)) . ')');
|
||||
} else {
|
||||
$sql->where('1 = 0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (int) Db::getInstance()->getValue($sql);
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ class EntitySelectorRenderer
|
||||
'required_message' => '',
|
||||
'show_modifiers' => true,
|
||||
'show_all_toggle' => false,
|
||||
'layout' => 'standalone', // 'standalone' (default), 'form-group' (full form-group), or 'form-content' (just content for existing form-group)
|
||||
];
|
||||
|
||||
$config = array_merge($defaults, $config);
|
||||
@@ -195,7 +196,21 @@ class EntitySelectorRenderer
|
||||
$jsConfigJson = $this->escapeAttr(json_encode($jsConfig));
|
||||
|
||||
$requiredClass = !empty($config['required']) ? ' trait-required' : '';
|
||||
$html = '<div class="condition-trait target-conditions-trait' . $collapsedClass . $singleModeClass . $requiredClass . '"';
|
||||
$layout = $config['layout'] ?? 'standalone';
|
||||
$layoutClass = $layout === 'form-group' ? ' layout-form-group' : '';
|
||||
|
||||
// Form-group layout: wrap in PrestaShop form structure
|
||||
if ($layout === 'form-group') {
|
||||
return $this->renderFormGroupLayout($config, $enabledBlocks, $activeBlock, $savedData, $globalMode, $jsConfigJson);
|
||||
}
|
||||
|
||||
// Form-content layout: just content for existing form-group (no wrapper, no extra label)
|
||||
if ($layout === 'form-content') {
|
||||
return $this->renderFormContentLayout($config, $enabledBlocks, $activeBlock, $savedData, $globalMode, $jsConfigJson);
|
||||
}
|
||||
|
||||
// Standalone layout (default)
|
||||
$html = '<div class="condition-trait target-conditions-trait' . $collapsedClass . $singleModeClass . $requiredClass . $layoutClass . '"';
|
||||
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
|
||||
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
|
||||
if (!empty($config['required'])) {
|
||||
@@ -232,6 +247,152 @@ class EntitySelectorRenderer
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render form-group layout (matches standard PrestaShop form fields)
|
||||
*
|
||||
* @param array $config Configuration
|
||||
* @param array $enabledBlocks Enabled blocks
|
||||
* @param string $activeBlock Active block type
|
||||
* @param array $savedData Saved data
|
||||
* @param string $globalMode Selection mode
|
||||
* @param string $jsConfigJson JSON-encoded JS config
|
||||
* @return string HTML
|
||||
*/
|
||||
protected function renderFormGroupLayout($config, $enabledBlocks, $activeBlock, $savedData, $globalMode, $jsConfigJson)
|
||||
{
|
||||
$requiredClass = !empty($config['required']) ? ' trait-required' : '';
|
||||
$singleModeClass = $globalMode === 'single' ? ' single-mode' : '';
|
||||
|
||||
$html = '<div class="form-group entity-selector-form-group">';
|
||||
|
||||
// Label column (col-lg-3) - standard PrestaShop form field markup
|
||||
$html .= '<label class="control-label col-lg-3">';
|
||||
$html .= '<span class="mpr-label-text">' . $this->escapeAttr($config['title']) . '</span>';
|
||||
if (!empty($config['required'])) {
|
||||
$html .= ' <span class="text-danger">*</span>';
|
||||
}
|
||||
$html .= '</label>';
|
||||
|
||||
// Content column (col-lg-9)
|
||||
$html .= '<div class="col-lg-9">';
|
||||
|
||||
// Entity selector container (without traditional header)
|
||||
$html .= '<div class="condition-trait target-conditions-trait layout-form-group' . $singleModeClass . $requiredClass . '"';
|
||||
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
|
||||
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
|
||||
if (!empty($config['required'])) {
|
||||
$html .= ' data-required="1"';
|
||||
$requiredMsg = !empty($config['required_message'])
|
||||
? $config['required_message']
|
||||
: $this->trans('Please select at least one item');
|
||||
$html .= ' data-required-message="' . $this->escapeAttr($requiredMsg) . '"';
|
||||
}
|
||||
$html .= ' data-config=\'' . $jsConfigJson . '\'>';
|
||||
|
||||
// Body (always visible in form-group layout)
|
||||
$html .= '<div class="condition-trait-body">';
|
||||
|
||||
// Tabs row with expand button
|
||||
$html .= '<div class="entity-selector-tabs-row">';
|
||||
$html .= $this->renderTabs($enabledBlocks, $activeBlock, $savedData, $config);
|
||||
|
||||
// Expand/collapse button for groups (multi mode only)
|
||||
if ($globalMode !== 'single') {
|
||||
$html .= '<div class="entity-selector-actions">';
|
||||
$html .= '<button type="button" class="btn-toggle-groups" data-state="collapsed" title="' . $this->trans('Expand all groups') . '">';
|
||||
$html .= '<i class="material-icons" style="font-size:18px;">unfold_more</i>';
|
||||
$html .= '</button>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
$html .= '</div>'; // End tabs-row
|
||||
|
||||
// Blocks
|
||||
$html .= $this->renderBlocks($enabledBlocks, $activeBlock, $savedData, $config, $globalMode);
|
||||
|
||||
// Tips box
|
||||
$html .= $this->renderTipsBox();
|
||||
|
||||
// Hidden input
|
||||
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
|
||||
|
||||
$html .= '</div>'; // End condition-trait-body
|
||||
$html .= '</div>'; // End target-conditions-trait
|
||||
|
||||
// Subtitle as help text
|
||||
if (!empty($config['subtitle'])) {
|
||||
$html .= '<small class="form-text text-muted">' . $this->escapeAttr($config['subtitle']) . '</small>';
|
||||
}
|
||||
|
||||
$html .= '</div>'; // End col-lg-9
|
||||
$html .= '</div>'; // End form-group
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render form-content layout (just content, no form-group wrapper)
|
||||
* Use this when entity selector is inside an existing PrestaShop form type='html' field
|
||||
*
|
||||
* @param array $config Configuration
|
||||
* @param array $enabledBlocks Enabled blocks
|
||||
* @param string $activeBlock Active block type
|
||||
* @param array $savedData Saved data
|
||||
* @param string $globalMode Selection mode
|
||||
* @param string $jsConfigJson JSON-encoded JS config
|
||||
* @return string HTML
|
||||
*/
|
||||
protected function renderFormContentLayout($config, $enabledBlocks, $activeBlock, $savedData, $globalMode, $jsConfigJson)
|
||||
{
|
||||
$requiredClass = !empty($config['required']) ? ' trait-required' : '';
|
||||
$singleModeClass = $globalMode === 'single' ? ' single-mode' : '';
|
||||
|
||||
// Check if blocks collapsed by default (default: true for form-content layout)
|
||||
$collapsed = isset($config['collapsed']) ? $config['collapsed'] : true;
|
||||
$collapsedClass = $collapsed ? ' blocks-collapsed' : '';
|
||||
|
||||
// Entity selector container (without form-group wrapper, without header)
|
||||
$html = '<div class="condition-trait target-conditions-trait layout-form-group' . $singleModeClass . $requiredClass . $collapsedClass . '"';
|
||||
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
|
||||
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
|
||||
if (!empty($config['required'])) {
|
||||
$html .= ' data-required="1"';
|
||||
$requiredMsg = !empty($config['required_message'])
|
||||
? $config['required_message']
|
||||
: $this->trans('Please select at least one item');
|
||||
$html .= ' data-required-message="' . $this->escapeAttr($requiredMsg) . '"';
|
||||
}
|
||||
$html .= ' data-config=\'' . $jsConfigJson . '\'>';
|
||||
|
||||
// Tabs row (always visible) with collapse toggle
|
||||
$html .= '<div class="entity-selector-tabs-row">';
|
||||
$html .= $this->renderTabs($enabledBlocks, $activeBlock, $savedData, $config);
|
||||
|
||||
// Actions: expand/collapse toggle (entire area is clickable)
|
||||
$html .= '<div class="entity-selector-actions btn-toggle-blocks" title="' . $this->trans('Show/hide details') . '">';
|
||||
$html .= '<i class="material-icons">' . ($collapsed ? 'expand_more' : 'expand_less') . '</i>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>'; // End tabs-row
|
||||
|
||||
// Blocks content (hidden when collapsed)
|
||||
$blocksStyle = $collapsed ? ' style="display:none;"' : '';
|
||||
$html .= '<div class="entity-selector-blocks-content"' . $blocksStyle . '>';
|
||||
|
||||
// Blocks
|
||||
$html .= $this->renderBlocks($enabledBlocks, $activeBlock, $savedData, $config, $globalMode);
|
||||
|
||||
// Tips box
|
||||
$html .= $this->renderTipsBox();
|
||||
|
||||
$html .= '</div>'; // End blocks-content
|
||||
|
||||
// Hidden input (outside collapsed area)
|
||||
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
|
||||
|
||||
$html .= '</div>'; // End target-conditions-trait
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render widget header
|
||||
*
|
||||
@@ -544,8 +705,8 @@ class EntitySelectorRenderer
|
||||
$html .= '<button type="button" class="btn-add-group">';
|
||||
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add selection group');
|
||||
$html .= '</button>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($groupsTooltip) . '">';
|
||||
$html .= '<i class="material-icons" style="font-size:16px;color:#5bc0de;cursor:pointer;vertical-align:middle">info</i>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($groupsTooltip) . '" data-toggle="none">';
|
||||
$html .= '<i class="material-icons">info</i>';
|
||||
$html .= '</span>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
@@ -606,8 +767,8 @@ class EntitySelectorRenderer
|
||||
$html .= '<i class="icon-sliders"></i> ';
|
||||
$html .= '<span class="result-modifiers-title">' . $this->trans('Result modifiers') . '</span>';
|
||||
$html .= '<span class="result-modifiers-hint">' . $this->trans('(optional)') . '</span>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($modifiersTooltip) . '">';
|
||||
$html .= '<i class="material-icons" style="font-size:16px;color:#5bc0de;cursor:pointer;vertical-align:middle">info</i>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($modifiersTooltip) . '" data-toggle="none">';
|
||||
$html .= '<i class="material-icons">info</i>';
|
||||
$html .= '</span>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -773,8 +934,8 @@ class EntitySelectorRenderer
|
||||
$methodHelp = $this->getMethodHelpTooltip($includeMethod, $blockType);
|
||||
$html .= '<span class="method-info-placeholder">';
|
||||
if (!empty($methodHelp)) {
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '">';
|
||||
$html .= '<i class="material-icons" style="font-size:16px;color:#5bc0de;cursor:pointer;vertical-align:middle">info</i>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '" data-toggle="none">';
|
||||
$html .= '<i class="material-icons">info</i>';
|
||||
$html .= '</span>';
|
||||
}
|
||||
$html .= '</span>';
|
||||
@@ -990,8 +1151,8 @@ class EntitySelectorRenderer
|
||||
$methodHelp = $this->getMethodHelpTooltip($excludeMethod, $blockType);
|
||||
$html .= '<span class="method-info-placeholder">';
|
||||
if (!empty($methodHelp)) {
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '">';
|
||||
$html .= '<i class="material-icons" style="font-size:16px;color:#5bc0de;cursor:pointer;vertical-align:middle">info</i>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '" data-toggle="none">';
|
||||
$html .= '<i class="material-icons">info</i>';
|
||||
$html .= '</span>';
|
||||
}
|
||||
$html .= '</span>';
|
||||
|
||||
@@ -209,6 +209,18 @@ trait ScheduleConditions
|
||||
$html .= '<span class="trait-title">' . htmlspecialchars($config['title']) . '</span>';
|
||||
$html .= '<span class="trait-subtitle">' . htmlspecialchars($config['subtitle']) . '</span>';
|
||||
$html .= '</div>';
|
||||
// Schedule summary (shows current configuration at a glance)
|
||||
$scheduleDescData = [
|
||||
'enabled' => $scheduleEnabled,
|
||||
'datetime_start' => isset($savedData['datetime_start']) ? $savedData['datetime_start'] : '',
|
||||
'datetime_end' => isset($savedData['datetime_end']) ? $savedData['datetime_end'] : '',
|
||||
'weekly_schedule' => $weeklySchedule,
|
||||
'exclude_holidays' => $excludeHolidays,
|
||||
'holiday_countries' => $holidayCountries,
|
||||
];
|
||||
$summaryText = $this->getScheduleDescription($scheduleDescData);
|
||||
$summaryVisible = $scheduleEnabled ? '' : ' style="display:none;"';
|
||||
$html .= '<span class="trait-summary"' . $summaryVisible . '>' . htmlspecialchars($summaryText) . '</span>';
|
||||
$html .= '</div>';
|
||||
// Expand/collapse all toggle - determine initial state based on section states
|
||||
$datetimeSectionCollapsed = empty($datetimeStart) && empty($datetimeEnd);
|
||||
@@ -788,7 +800,7 @@ trait ScheduleConditions
|
||||
|
||||
$jsConfigJson = htmlspecialchars(json_encode($jsConfig), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$html = '<div class="target-conditions-trait holiday-countries-target"';
|
||||
$html = '<div class="target-conditions-trait holiday-countries-target layout-form-group"';
|
||||
$html .= ' data-entity-selector-id="holiday-countries-target"';
|
||||
$html .= ' data-mode="multi"';
|
||||
$html .= ' data-config=\'' . $jsConfigJson . '\'>';
|
||||
@@ -843,8 +855,8 @@ trait ScheduleConditions
|
||||
$html .= '<button type="button" class="btn-add-group">';
|
||||
$html .= '<i class="icon-plus"></i> ' . $this->transScheduleConditions('Add selection group');
|
||||
$html .= '</button>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . htmlspecialchars($groupsTooltip) . '">';
|
||||
$html .= '<i class="material-icons" style="font-size:16px;color:#5bc0de;cursor:pointer;vertical-align:middle">info</i>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . htmlspecialchars($groupsTooltip) . '" data-toggle="none">';
|
||||
$html .= '<i class="material-icons">info</i>';
|
||||
$html .= '</span>';
|
||||
$html .= '</div>';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user