FEAT_RULE_FILTER
This commit is contained in:
parent
fded069957
commit
528458be46
@ -5,12 +5,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
display: flex;
|
/* display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between; */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
background: white;
|
background-color: #F5FFFF !important;
|
||||||
padding: 20px;
|
padding: 15px 25px 10px 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar-header {
|
||||||
|
/* display: flex;
|
||||||
|
justify-content: space-between; */
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background-color: #F5FFFF !important;
|
||||||
|
padding: 0px 0px 0px 25px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
@ -29,7 +40,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rule-card {
|
.rule-card {
|
||||||
background: white;
|
background:#F5FFFF !important;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
|
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
|
||||||
@ -85,11 +96,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.condition-badge {
|
.condition-badge {
|
||||||
|
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
font-size: 7px;
|
font-size: 7px;
|
||||||
color: white;
|
display: inline-block;
|
||||||
background: linear-gradient(45deg, #3498db, #2980b9);
|
border-radius: 6px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
margin-right: 6px;
|
||||||
|
color: #222;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-item {
|
.detail-item {
|
||||||
@ -98,8 +115,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-label {
|
.detail-label {
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
color: #000000;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ruleModal .modal-body {
|
#ruleModal .modal-body {
|
||||||
@ -220,26 +237,140 @@
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select2-hidden-accessible + .select2-container .select2-dropdown {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container .select2-selection--multiple .select2-selection__choice {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-selection__choice {
|
||||||
|
background-color: #0a8794 !important;
|
||||||
|
color: white !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-selection__choice__remove {
|
||||||
|
color: white !important;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-filters {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-rulesList {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* layout for rulesList area */
|
||||||
|
#rulesList {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<!-- Action Bar -->
|
<div id="accordion" class="mb-3">
|
||||||
|
|
||||||
|
<h4 class="action-bar-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||||
|
<span>Filter Rules</span>
|
||||||
|
|
||||||
|
<a id="toggleIcon" class="text-dark"
|
||||||
|
data-toggle="collapse"
|
||||||
|
href="#collapseOne"
|
||||||
|
aria-expanded="true"
|
||||||
|
style="margin-left:auto;">
|
||||||
|
<i id="icon" class="mdi mdi-chevron-down mr-1 text-primary" style="font-size:35px;"></i>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div id="collapseOne" class="collapse" aria-labelledby="headingOne" data-parent="#accordion">
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<div class="filter-section">
|
|
||||||
<label for="filterDepartment">Filter:</label>
|
<div class="row">
|
||||||
<select class="form-control" id="filterDepartment" onchange="filterRules()">
|
|
||||||
<option value="">All Departments</option>
|
<div class="form-group col-md-6">
|
||||||
<option value="Health">Health</option>
|
<input class="form-control" id="searchName" type="text" placeholder="Search rule name (partial, case-insensitive)" />
|
||||||
<option value="Motor">Motor</option>
|
</div>
|
||||||
<option value="Fire">Fire</option>
|
|
||||||
<option value="Travel">Travel</option>
|
<div class="form-group col-md-3">
|
||||||
<option value="Marine">Marine</option>
|
<select class="form-control" id="calcType">
|
||||||
|
<option value="">Calc type — Any</option>
|
||||||
|
<option value="percentage">percentage</option>
|
||||||
|
<option value="fixed">fixed</option>
|
||||||
|
<option value="composite">composite</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group col-md-3">
|
||||||
|
<select class="form-control" id="deletedFilter">
|
||||||
|
<option value="any">Status — Any</option>
|
||||||
|
<option value="active">Only active</option>
|
||||||
|
<option value="deleted">Only deleted</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6" style="margin-top: 0;">
|
||||||
|
<!-- <label style="display:block; font-size:13px; margin-bottom:6px">Has Field (any value)</label> -->
|
||||||
|
<select class="form-control" id="hasField" multiple></select>
|
||||||
|
<div style="margin-top:6px;" class="invert-label">
|
||||||
|
<input id="invertHasField" type="checkbox" />
|
||||||
|
<label for="invertHasField" class="muted">Invert — Does NOT contain selected fields</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-3" style="margin-top: 0;">
|
||||||
|
<input class="form-control" id="minConds" type="number" min="0" placeholder="Rule Conditions Min:" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-3" style="margin-top: 0;">
|
||||||
|
<input class="form-control" id="maxConds" type="number" min="0" placeholder="Rule Conditions Max:" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>Add more filters (field-level) <small class="muted">(Add arbitrary condition-field filters (e.g., vehicle_type, fuel_type, policy_type). Each row is ANDed together.)</small></strong>
|
||||||
|
<div id="dynamicFilters" class="dynamic-filters"></div>
|
||||||
|
<div style="margin-top:15px;display: flex;justify-content: space-between;">
|
||||||
|
|
||||||
|
<div class="form-group text-left m-b-0">
|
||||||
<button class="btn btn-primary" onclick="openModal()">Create New Rule</button>
|
<button class="btn btn-primary" onclick="openModal()">Create New Rule</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group text-right m-b-0">
|
||||||
|
<button id="addFilterBtn" class="btn btn-success btn-small">+ Add filter</button>
|
||||||
|
<button id="resetBtn" class="btn btn-secondary btn-small" style="margin-left:8px">Reset</button>
|
||||||
|
<button id="applyBtn" class="btn btn-primary btn-small">Apply</button>
|
||||||
|
<span id="rulesListCount" style="margin-left:12px" class=""></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Rules Grid -->
|
<!-- Rules Grid -->
|
||||||
<div id="rulesList" class="rules-grid">
|
<div id="rulesList" class="rules-grid">
|
||||||
<!-- Rules will be displayed here as cards -->
|
<!-- Rules will be displayed here as cards -->
|
||||||
@ -385,7 +516,7 @@
|
|||||||
<option value=">">>(greater than)</option>
|
<option value=">">>(greater than)</option>
|
||||||
<option value="<"><(less than)</option>
|
<option value="<"><(less than)</option>
|
||||||
<option value=">=">>=(greater than or equal)</option>
|
<option value=">=">>=(greater than or equal)</option>
|
||||||
<option value="<=">>=(less than or equal)</option>
|
<option value="<="><=(less than or equal)</option>
|
||||||
<option value="between">between</option>
|
<option value="between">between</option>
|
||||||
<option value="in">in (array)</option>
|
<option value="in">in (array)</option>
|
||||||
</select>
|
</select>
|
||||||
@ -767,7 +898,7 @@
|
|||||||
|
|
||||||
function displayRules(sucess_data = null) {
|
function displayRules(sucess_data = null) {
|
||||||
|
|
||||||
const filter = document.getElementById('filterDepartment').value;
|
// const filter = document.getElementById('filterDepartment').value ?? null;
|
||||||
const container = document.getElementById('rulesList');
|
const container = document.getElementById('rulesList');
|
||||||
|
|
||||||
let filteredRules = sucess_data ?? rules;
|
let filteredRules = sucess_data ?? rules;
|
||||||
@ -787,9 +918,9 @@
|
|||||||
// console.log('filteredRules', filteredRules);
|
// console.log('filteredRules', filteredRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter) {
|
// if (filter) {
|
||||||
filteredRules = rules.filter(r => r.department === filter);
|
// filteredRules = rules.filter(r => r.department === filter);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (filteredRules.length === 0) {
|
if (filteredRules.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
@ -907,7 +1038,587 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
$("#hasField").select2({
|
||||||
|
tags: true,
|
||||||
|
placeholder: "Has Field (any value)",
|
||||||
|
tokenSeparators: [], // Disable default token separators
|
||||||
|
createTag: function (params) {
|
||||||
|
var term = $.trim(params.term);
|
||||||
|
if (term === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: term,
|
||||||
|
text: term,
|
||||||
|
newTag: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$("textarea.select2-search__field").attr('rows', '1');
|
||||||
|
$("textarea.select2-search__field").css('resize', 'none');
|
||||||
|
});
|
||||||
|
|
||||||
// Initial display
|
// Initial display
|
||||||
displayRules();
|
displayRules();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/***** SEARCH & RENDER SCRIPT (renders new card HTML) *****/
|
||||||
|
(function () {
|
||||||
|
// DOM refs
|
||||||
|
const dynamicFiltersEl = document.getElementById('dynamicFilters');
|
||||||
|
const addFilterBtn = document.getElementById('addFilterBtn');
|
||||||
|
const applyBtn = document.getElementById('applyBtn');
|
||||||
|
const resetBtn = document.getElementById('resetBtn');
|
||||||
|
const rulesListEl = document.getElementById('rulesList');
|
||||||
|
const rulesListCount = document.getElementById('rulesListCount');
|
||||||
|
|
||||||
|
let parse_rules = JSON.parse(rules);
|
||||||
|
|
||||||
|
// Utility: build field -> values map from conditions across rules
|
||||||
|
function buildFieldValueMap(parse_rules) {
|
||||||
|
const map = {};
|
||||||
|
(parse_rules || []).forEach(r => {
|
||||||
|
(r.conditions || []).forEach(c => {
|
||||||
|
const f = c.field || 'unknown';
|
||||||
|
if (!map[f]) map[f] = new Set();
|
||||||
|
if (Array.isArray(c.value)) {
|
||||||
|
c.value.forEach(v => map[f].add(String(v)));
|
||||||
|
} else {
|
||||||
|
map[f].add(String(c.value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const out = {};
|
||||||
|
Object.keys(map).forEach(k => out[k] = Array.from(map[k]).sort());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const fieldValueMap = buildFieldValueMap(parse_rules);
|
||||||
|
|
||||||
|
// Populate Has Field multi-select
|
||||||
|
(function populateHasField() {
|
||||||
|
const sel = document.getElementById("hasField");
|
||||||
|
if (!sel) return;
|
||||||
|
sel.innerHTML = '';
|
||||||
|
const fields = Object.keys(fieldValueMap).sort();
|
||||||
|
fields.forEach(f => {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = f;
|
||||||
|
opt.textContent = f;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ----- Dynamic filter row creation -----
|
||||||
|
function addFilterRow(prefill) {
|
||||||
|
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'filter-row';
|
||||||
|
|
||||||
|
// field select
|
||||||
|
const fieldSel = document.createElement('select');
|
||||||
|
fieldSel.classList.add('form-control');
|
||||||
|
const placeholderOption = document.createElement('option');
|
||||||
|
placeholderOption.value = '';
|
||||||
|
placeholderOption.textContent = 'field';
|
||||||
|
fieldSel.appendChild(placeholderOption);
|
||||||
|
|
||||||
|
const fields = Object.keys(fieldValueMap).sort();
|
||||||
|
fields.forEach(f => {
|
||||||
|
const o = document.createElement('option'); o.value = f; o.textContent = f;
|
||||||
|
fieldSel.appendChild(o);
|
||||||
|
});
|
||||||
|
|
||||||
|
const customFieldOpt = document.createElement('option');
|
||||||
|
customFieldOpt.value = '__custom';
|
||||||
|
customFieldOpt.textContent = '-- custom field --';
|
||||||
|
fieldSel.appendChild(customFieldOpt);
|
||||||
|
|
||||||
|
// operator select
|
||||||
|
const operatorSel = document.createElement('select');
|
||||||
|
operatorSel.classList.add('form-control');
|
||||||
|
['==', 'in', 'contains'].forEach(op => {
|
||||||
|
const o = document.createElement('option'); o.value = op; o.textContent = op;
|
||||||
|
operatorSel.appendChild(o);
|
||||||
|
});
|
||||||
|
|
||||||
|
// value input (for the predicate value)
|
||||||
|
const valueInput = document.createElement('input');
|
||||||
|
valueInput.classList.add('form-control');
|
||||||
|
valueInput.type = 'text';
|
||||||
|
valueInput.placeholder = 'value (for "in" use comma separated)';
|
||||||
|
valueInput.style.minWidth = '220px';
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className = 'btn btn-danger btn-small';
|
||||||
|
removeBtn.type = 'button';
|
||||||
|
removeBtn.textContent = '✖';
|
||||||
|
removeBtn.onclick = () => row.remove();
|
||||||
|
|
||||||
|
// prefill support
|
||||||
|
if (prefill) {
|
||||||
|
if (fields.includes(prefill.field)) fieldSel.value = prefill.field;
|
||||||
|
else fieldSel.value = '__custom';
|
||||||
|
operatorSel.value = prefill.operator || '==';
|
||||||
|
if (Array.isArray(prefill.value)) valueInput.value = prefill.value.join(',');
|
||||||
|
else valueInput.value = prefill.value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// when select custom field, create a custom input element (class .custom-field)
|
||||||
|
fieldSel.addEventListener('change', () => {
|
||||||
|
if (fieldSel.value === '__custom') {
|
||||||
|
if (!row.querySelector('.custom-field')) {
|
||||||
|
const text = document.createElement('input');
|
||||||
|
text.type = 'text';
|
||||||
|
text.className = 'custom-field form-control';
|
||||||
|
text.placeholder = 'custom field name';
|
||||||
|
text.style.minWidth = '140px';
|
||||||
|
row.insertBefore(text, operatorSel);
|
||||||
|
fieldSel.style.display = 'none';
|
||||||
|
text.focus();
|
||||||
|
|
||||||
|
text.addEventListener('input', () => {
|
||||||
|
text.dataset.fieldName = text.value.trim();
|
||||||
|
});
|
||||||
|
text.addEventListener('blur', () => {
|
||||||
|
const val = text.value.trim();
|
||||||
|
if (!val) {
|
||||||
|
text.remove();
|
||||||
|
fieldSel.style.display = '';
|
||||||
|
fieldSel.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const textInput = row.querySelector('.custom-field');
|
||||||
|
if (textInput) textInput.remove();
|
||||||
|
fieldSel.style.display = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(fieldSel);
|
||||||
|
row.appendChild(operatorSel);
|
||||||
|
row.appendChild(valueInput);
|
||||||
|
row.appendChild(removeBtn);
|
||||||
|
dynamicFiltersEl.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
addFilterBtn.addEventListener('click', () => addFilterRow());
|
||||||
|
|
||||||
|
// Helper: clear multi-select (cross-browser)
|
||||||
|
function clearMultiSelect(sel) {
|
||||||
|
if (!sel) return;
|
||||||
|
Array.from(sel.options).forEach(o => o.selected = false);
|
||||||
|
try { sel.selectedIndex = -1; } catch (e) {/*ignore*/ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset handler
|
||||||
|
resetBtn.addEventListener('click', () => {
|
||||||
|
document.getElementById('searchName').value = '';
|
||||||
|
document.getElementById('calcType').value = '';
|
||||||
|
document.getElementById('deletedFilter').value = 'any';
|
||||||
|
document.getElementById('minConds').value = '';
|
||||||
|
document.getElementById('maxConds').value = '';
|
||||||
|
clearMultiSelect(document.getElementById('hasField'));
|
||||||
|
document.getElementById('invertHasField').checked = false;
|
||||||
|
dynamicFiltersEl.innerHTML = '';
|
||||||
|
addFilterRow(); // keep a starter empty row
|
||||||
|
renderrulesList(parse_rules);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build predicates from dynamic filter rows
|
||||||
|
function buildFieldPredicates() {
|
||||||
|
const rows = Array.from(dynamicFiltersEl.querySelectorAll('.filter-row'));
|
||||||
|
const preds = [];
|
||||||
|
rows.forEach(row => {
|
||||||
|
// field name: prefer .custom-field if present
|
||||||
|
const customFieldInput = row.querySelector('.custom-field');
|
||||||
|
let fieldName = '';
|
||||||
|
if (customFieldInput) {
|
||||||
|
fieldName = customFieldInput.dataset.fieldName ? customFieldInput.dataset.fieldName : customFieldInput.value.trim();
|
||||||
|
} else {
|
||||||
|
const sel = row.querySelector('select');
|
||||||
|
if (sel) fieldName = (sel.value === '__custom') ? '' : sel.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const op = row.querySelectorAll('select')[1]?.value || '==';
|
||||||
|
// find the non-custom value input
|
||||||
|
const inputs = row.querySelectorAll('input[type="text"]');
|
||||||
|
let valInput = null;
|
||||||
|
inputs.forEach(inp => { if (!inp.classList.contains('custom-field')) valInput = inp; });
|
||||||
|
const val = valInput ? valInput.value.trim() : '';
|
||||||
|
|
||||||
|
if (!fieldName || val === '') return; // ignore incomplete rows
|
||||||
|
|
||||||
|
preds.push(rule => {
|
||||||
|
const conds = rule.conditions || [];
|
||||||
|
if (op === '==') {
|
||||||
|
return conds.some(c => String(c.field) === String(fieldName) && String(Array.isArray(c.value) ? c.value.join(',') : c.value) === val);
|
||||||
|
} else if (op === 'in') {
|
||||||
|
const requested = val.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
return conds.some(c => {
|
||||||
|
if (Array.isArray(c.value)) return requested.some(r => c.value.map(String).includes(r));
|
||||||
|
return requested.includes(String(c.value));
|
||||||
|
});
|
||||||
|
} else if (op === 'contains') {
|
||||||
|
return conds.some(c => {
|
||||||
|
if (Array.isArray(c.value)) return c.value.some(v => String(v).toLowerCase().includes(val.toLowerCase()));
|
||||||
|
return String(c.value).toLowerCase().includes(val.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return preds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply all filters
|
||||||
|
function applyFilters() {
|
||||||
|
const nameFilter = document.getElementById('searchName').value.trim().toLowerCase();
|
||||||
|
const calcType = document.getElementById('calcType').value;
|
||||||
|
const deletedFilter = document.getElementById('deletedFilter').value; // any | active | deleted
|
||||||
|
const minConds = document.getElementById('minConds').value;
|
||||||
|
const maxConds = document.getElementById('maxConds').value;
|
||||||
|
|
||||||
|
// Has Field multi-select and invert flag
|
||||||
|
const hasFieldSelect = document.getElementById('hasField');
|
||||||
|
const selectedOptions = hasFieldSelect ? Array.from(hasFieldSelect.selectedOptions).map(o => o.value).filter(Boolean) : [];
|
||||||
|
const invertHas = document.getElementById('invertHasField').checked;
|
||||||
|
|
||||||
|
const fieldPreds = buildFieldPredicates();
|
||||||
|
|
||||||
|
const filtered = (parse_rules || []).filter(rule => {
|
||||||
|
// name
|
||||||
|
if (nameFilter) {
|
||||||
|
if (!String(rule.name || '').toLowerCase().includes(nameFilter)) return false;
|
||||||
|
}
|
||||||
|
// calc type
|
||||||
|
if (calcType) {
|
||||||
|
if (!rule.calculation || rule.calculation.type !== calcType) return false;
|
||||||
|
}
|
||||||
|
// deleted
|
||||||
|
if (deletedFilter === 'active' && rule.is_deleted) return false;
|
||||||
|
if (deletedFilter === 'deleted' && !rule.is_deleted) return false;
|
||||||
|
// condition count
|
||||||
|
const condCount = (rule.conditions || []).length;
|
||||||
|
if (minConds !== '' && Number(minConds) > condCount) return false;
|
||||||
|
if (maxConds !== '' && Number(maxConds) < condCount) return false;
|
||||||
|
// dynamic field predicates (AND them)
|
||||||
|
for (const p of fieldPreds) {
|
||||||
|
if (!p(rule)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has Field logic (OR semantics across selected fields)
|
||||||
|
if (selectedOptions.length > 0) {
|
||||||
|
const condFields = (rule.conditions || []).map(c => String(c.field));
|
||||||
|
const containsAny = selectedOptions.some(f => condFields.includes(f));
|
||||||
|
if (!invertHas && !containsAny) return false;
|
||||||
|
if (invertHas && containsAny) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderrulesList(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyBtn.addEventListener('click', applyFilters);
|
||||||
|
|
||||||
|
// Render rulesList using your new card HTML structure
|
||||||
|
function renderrulesList(list) {
|
||||||
|
rulesListEl.innerHTML = '';
|
||||||
|
rulesListCount.textContent = `${(list || []).length} result(s)`;
|
||||||
|
if (!list || !list.length) {
|
||||||
|
rulesListEl.innerHTML = '<div class="no-rulesList">No rules found for these filters.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// list.forEach(rule => {
|
||||||
|
// // card root
|
||||||
|
// const card = document.createElement('div');
|
||||||
|
// card.className = 'rule-card' + (rule.is_deleted ? ' deleted' : '');
|
||||||
|
|
||||||
|
// // header
|
||||||
|
// const header = document.createElement('div');
|
||||||
|
// header.className = 'rule-card-header';
|
||||||
|
|
||||||
|
// const left = document.createElement('div');
|
||||||
|
// const nameDiv = document.createElement('div');
|
||||||
|
// nameDiv.className = 'rule-name';
|
||||||
|
// nameDiv.textContent = rule.name || '—';
|
||||||
|
// left.appendChild(nameDiv);
|
||||||
|
// header.appendChild(left);
|
||||||
|
|
||||||
|
// const dept = document.createElement('span');
|
||||||
|
// dept.className = 'department-badge';
|
||||||
|
// dept.textContent = rule.department || '—';
|
||||||
|
// header.appendChild(dept);
|
||||||
|
|
||||||
|
// card.appendChild(header);
|
||||||
|
|
||||||
|
// // details
|
||||||
|
// const details = document.createElement('div');
|
||||||
|
// details.className = 'rule-details';
|
||||||
|
|
||||||
|
// // Calculation Type
|
||||||
|
// const calcTypeText = (rule.calculation && rule.calculation.type) ? rule.calculation.type : 'N/A';
|
||||||
|
// const calcTypeDiv = document.createElement('div');
|
||||||
|
// calcTypeDiv.className = 'detail-item';
|
||||||
|
// calcTypeDiv.innerHTML = `<span class="detail-label">Calculation Type:</span> ${escapeHtml(calcTypeText)}`;
|
||||||
|
// details.appendChild(calcTypeDiv);
|
||||||
|
|
||||||
|
// // Value (show percentage sign for percentage type)
|
||||||
|
// const calcValueDiv = document.createElement('div');
|
||||||
|
// calcValueDiv.className = 'detail-item';
|
||||||
|
// let calcValueText = 'N/A';
|
||||||
|
// if (rule.calculation) {
|
||||||
|
// if (rule.calculation.type === 'percentage' && typeof rule.calculation.value !== 'undefined') {
|
||||||
|
// calcValueText = String(rule.calculation.value) + '%';
|
||||||
|
// } else if (rule.calculation.type === 'fixed' && typeof rule.calculation.value !== 'undefined') {
|
||||||
|
// calcValueText = String(rule.calculation.value);
|
||||||
|
// } else if (rule.calculation.type === 'composite' && Array.isArray(rule.calculation.components)) {
|
||||||
|
// calcValueText = rule.calculation.components.map(c => (c.percentage !== undefined ? (c.percentage + '% on ' + (c.on || 'N/A')) : JSON.stringify(c))).join(', ');
|
||||||
|
// } else if (typeof rule.calculation.value !== 'undefined') {
|
||||||
|
// calcValueText = String(rule.calculation.value);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// calcValueDiv.innerHTML = `<span class="detail-label">Value:</span> ${escapeHtml(calcValueText)}`;
|
||||||
|
// details.appendChild(calcValueDiv);
|
||||||
|
|
||||||
|
// // Applied On
|
||||||
|
// const appliedOnDiv = document.createElement('div');
|
||||||
|
// appliedOnDiv.className = 'detail-item';
|
||||||
|
// let appliedOnText = 'N/A';
|
||||||
|
// if (rule.calculation) {
|
||||||
|
// if (rule.calculation.on) appliedOnText = rule.calculation.on;
|
||||||
|
// else if (rule.calculation.type === 'composite' && Array.isArray(rule.calculation.components)) {
|
||||||
|
// appliedOnText = rule.calculation.components.map(c => c.on || 'N/A').join(', ');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// appliedOnDiv.innerHTML = `<span class="detail-label">Applied On:</span> ${escapeHtml(appliedOnText)}`;
|
||||||
|
// details.appendChild(appliedOnDiv);
|
||||||
|
|
||||||
|
// // Conditions count
|
||||||
|
// const condCountDiv = document.createElement('div');
|
||||||
|
// condCountDiv.className = 'detail-item';
|
||||||
|
// const condCount = (rule.conditions || []).length;
|
||||||
|
// condCountDiv.innerHTML = `<span class="detail-label">Conditions:</span> ${condCount} condition(s)`;
|
||||||
|
// details.appendChild(condCountDiv);
|
||||||
|
|
||||||
|
// card.appendChild(details);
|
||||||
|
|
||||||
|
// // condition badges inline (as in your example, simple spans)
|
||||||
|
// const conds = rule.conditions || [];
|
||||||
|
// const badgesWrap = document.createElement('div');
|
||||||
|
// badgesWrap.style.marginTop = '8px';
|
||||||
|
// conds.forEach(c => {
|
||||||
|
// const b = document.createElement('span');
|
||||||
|
// b.className = 'condition-badge';
|
||||||
|
// b.textContent = c.field || '—';
|
||||||
|
// b.style.marginRight = '6px';
|
||||||
|
// badgesWrap.appendChild(b);
|
||||||
|
// });
|
||||||
|
// card.appendChild(badgesWrap);
|
||||||
|
|
||||||
|
// // actions
|
||||||
|
// const actions = document.createElement('div');
|
||||||
|
// actions.className = 'rule-actions';
|
||||||
|
// actions.style.marginTop = '8px';
|
||||||
|
|
||||||
|
// const editBtn = document.createElement('button');
|
||||||
|
// editBtn.className = 'btn btn-primary btn-small';
|
||||||
|
// editBtn.type = 'button';
|
||||||
|
// editBtn.setAttribute('aria-label', 'Edit rule');
|
||||||
|
// // inline pencil SVG
|
||||||
|
// editBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
// <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25z" fill="currentColor"/>
|
||||||
|
// <path d="M20.71 7.04a1 1 0 0 0 0-1.41L18.37 3.29a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" fill="currentColor"/>
|
||||||
|
// </svg>`;
|
||||||
|
// editBtn.addEventListener('click', () => editRule(rule.id));
|
||||||
|
// actions.appendChild(editBtn);
|
||||||
|
|
||||||
|
// const delBtn = document.createElement('button');
|
||||||
|
// delBtn.className = 'btn btn-danger btn-small';
|
||||||
|
// delBtn.type = 'button';
|
||||||
|
// delBtn.setAttribute('aria-label', 'Delete rule');
|
||||||
|
// // inline trash SVG
|
||||||
|
// delBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
// <path d="M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6v12z" fill="currentColor"/>
|
||||||
|
// <path d="M19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="currentColor"/>
|
||||||
|
// </svg>`;
|
||||||
|
// delBtn.addEventListener('click', () => deleteRule(rule.id));
|
||||||
|
// actions.appendChild(delBtn);
|
||||||
|
|
||||||
|
// card.appendChild(actions);
|
||||||
|
|
||||||
|
// rulesListEl.appendChild(card);
|
||||||
|
// });
|
||||||
|
|
||||||
|
list.forEach(rule => {
|
||||||
|
// card root
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'rule-card' + (rule.is_deleted ? ' deleted' : '');
|
||||||
|
|
||||||
|
// header
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'rule-card-header';
|
||||||
|
|
||||||
|
const left = document.createElement('div');
|
||||||
|
const nameDiv = document.createElement('div');
|
||||||
|
nameDiv.className = 'rule-name';
|
||||||
|
nameDiv.textContent = rule.name || '—';
|
||||||
|
left.appendChild(nameDiv);
|
||||||
|
header.appendChild(left);
|
||||||
|
|
||||||
|
const dept = document.createElement('span');
|
||||||
|
dept.className = 'department-badge';
|
||||||
|
dept.textContent = rule.department.charAt(0).toUpperCase() + rule.department.slice(1) || '—';
|
||||||
|
header.appendChild(dept);
|
||||||
|
|
||||||
|
card.appendChild(header);
|
||||||
|
|
||||||
|
// details
|
||||||
|
const details = document.createElement('div');
|
||||||
|
details.className = 'rule-details';
|
||||||
|
|
||||||
|
// Calculation Type
|
||||||
|
const calcTypeText = (rule.calculation && rule.calculation.type) ? rule.calculation.type : 'N/A';
|
||||||
|
const calcTypeDiv = document.createElement('div');
|
||||||
|
calcTypeDiv.className = 'detail-item';
|
||||||
|
calcTypeDiv.innerHTML = `<span class="detail-label">Calculation Type:</span> ${escapeHtml(calcTypeText)}`;
|
||||||
|
details.appendChild(calcTypeDiv);
|
||||||
|
|
||||||
|
// Value
|
||||||
|
const calcValueDiv = document.createElement('div');
|
||||||
|
calcValueDiv.className = 'detail-item';
|
||||||
|
let calcValueText = 'N/A';
|
||||||
|
if (rule.calculation) {
|
||||||
|
if (rule.calculation.type === 'percentage' && typeof rule.calculation.value !== 'undefined') {
|
||||||
|
calcValueText = String(rule.calculation.value) + '%';
|
||||||
|
} else if (rule.calculation.type === 'fixed' && typeof rule.calculation.value !== 'undefined') {
|
||||||
|
calcValueText = String(rule.calculation.value);
|
||||||
|
} else if (rule.calculation.type === 'composite' && Array.isArray(rule.calculation.components)) {
|
||||||
|
calcValueText = rule.calculation.components
|
||||||
|
.map(c => (c.percentage !== undefined ? (c.percentage + '% on ' + (c.on || 'N/A')) : ''))
|
||||||
|
.join(', ');
|
||||||
|
} else if (typeof rule.calculation.value !== 'undefined') {
|
||||||
|
calcValueText = String(rule.calculation.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
calcValueDiv.innerHTML = `<span class="detail-label">Value:</span> ${escapeHtml(calcValueText)}`;
|
||||||
|
details.appendChild(calcValueDiv);
|
||||||
|
|
||||||
|
// Applied On
|
||||||
|
const appliedOnDiv = document.createElement('div');
|
||||||
|
appliedOnDiv.className = 'detail-item';
|
||||||
|
let appliedOnText = 'N/A';
|
||||||
|
if (rule.calculation) {
|
||||||
|
if (rule.calculation.on) appliedOnText = rule.calculation.on;
|
||||||
|
else if (rule.calculation.type === 'composite' && Array.isArray(rule.calculation.components)) {
|
||||||
|
appliedOnText = rule.calculation.components.map(c => c.on || 'N/A').join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appliedOnDiv.innerHTML = `<span class="detail-label">Applied On:</span> ${escapeHtml(appliedOnText)}`;
|
||||||
|
details.appendChild(appliedOnDiv);
|
||||||
|
|
||||||
|
// Conditions count
|
||||||
|
const condCountDiv = document.createElement('div');
|
||||||
|
condCountDiv.className = 'detail-item';
|
||||||
|
const condCount = (rule.conditions || []).length;
|
||||||
|
condCountDiv.innerHTML = `<span class="detail-label">Conditions:</span> ${condCount} condition(s)`;
|
||||||
|
details.appendChild(condCountDiv);
|
||||||
|
|
||||||
|
card.appendChild(details);
|
||||||
|
|
||||||
|
// Condition badges
|
||||||
|
const conds = rule.conditions || [];
|
||||||
|
const badgesWrap = document.createElement('div');
|
||||||
|
badgesWrap.style.marginTop = '8px';
|
||||||
|
conds.forEach(c => {
|
||||||
|
const b = document.createElement('span');
|
||||||
|
b.className = 'condition-badge';
|
||||||
|
b.textContent = c.field || '—';
|
||||||
|
b.style.marginRight = '6px';
|
||||||
|
badgesWrap.appendChild(b);
|
||||||
|
});
|
||||||
|
card.appendChild(badgesWrap);
|
||||||
|
|
||||||
|
// actions
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'rule-actions';
|
||||||
|
actions.style.marginTop = '8px';
|
||||||
|
|
||||||
|
if (rule.is_deleted) {
|
||||||
|
// Deleted badge
|
||||||
|
const deletedBadge = document.createElement('span');
|
||||||
|
deletedBadge.className = 'btn btn-danger btn-small disabled';
|
||||||
|
deletedBadge.textContent = 'Deleted';
|
||||||
|
actions.appendChild(deletedBadge);
|
||||||
|
} else {
|
||||||
|
// Edit button
|
||||||
|
const editBtn = document.createElement('button');
|
||||||
|
editBtn.className = 'btn btn-primary btn-small';
|
||||||
|
editBtn.type = 'button';
|
||||||
|
editBtn.setAttribute('aria-label', 'Edit rule');
|
||||||
|
editBtn.innerHTML = `
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25z" fill="currentColor"/>
|
||||||
|
<path d="M20.71 7.04a1 1 0 0 0 0-1.41L18.37 3.29a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" fill="currentColor"/>
|
||||||
|
</svg>`;
|
||||||
|
editBtn.addEventListener('click', () => editRule(rule.id));
|
||||||
|
actions.appendChild(editBtn);
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
const delBtn = document.createElement('button');
|
||||||
|
delBtn.className = 'btn btn-danger btn-small';
|
||||||
|
delBtn.type = 'button';
|
||||||
|
delBtn.setAttribute('aria-label', 'Delete rule');
|
||||||
|
delBtn.innerHTML = `
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6v12z" fill="currentColor"/>
|
||||||
|
<path d="M19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="currentColor"/>
|
||||||
|
</svg>`;
|
||||||
|
delBtn.addEventListener('click', () => deleteRule(rule.id));
|
||||||
|
actions.appendChild(delBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.appendChild(actions);
|
||||||
|
|
||||||
|
// append to DOM
|
||||||
|
rulesListEl.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// small utility to safely escape text inserted into innerHTML strings
|
||||||
|
function escapeHtml(s) {
|
||||||
|
if (s === null || s === undefined) return '';
|
||||||
|
return String(s).replace(/[&<>"']/g, function (m) {
|
||||||
|
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[m];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// initial setup: one dynamic filter row and initial render of all rules
|
||||||
|
addFilterRow();
|
||||||
|
renderrulesList(parse_rules);
|
||||||
|
|
||||||
|
// Optional: dynamically add calc types discovered to calcType select
|
||||||
|
(function dynamicCalcTypes() {
|
||||||
|
const s = document.getElementById('calcType');
|
||||||
|
if (!s) return;
|
||||||
|
const types = Array.from(new Set((parse_rules || []).map(r => r.calculation && r.calculation.type).filter(Boolean)));
|
||||||
|
types.forEach(t => {
|
||||||
|
if (![...s.options].some(o => o.value === t)) {
|
||||||
|
const o = document.createElement('option'); o.value = t; o.textContent = t;
|
||||||
|
s.appendChild(o);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user