FEAT_RULE_FILTER
This commit is contained in:
parent
fded069957
commit
528458be46
@ -5,12 +5,23 @@
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* display: flex;
|
||||
justify-content: space-between; */
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
background-color: #F5FFFF !important;
|
||||
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;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
@ -29,7 +40,7 @@
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
background: white;
|
||||
background:#F5FFFF !important;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
|
||||
@ -85,11 +96,17 @@
|
||||
}
|
||||
|
||||
.condition-badge {
|
||||
|
||||
padding: 2px 5px;
|
||||
border-radius: 15px;
|
||||
font-size: 7px;
|
||||
color: white;
|
||||
background: linear-gradient(45deg, #3498db, #2980b9);
|
||||
display: inline-block;
|
||||
border-radius: 6px;
|
||||
background: #f1f5f9;
|
||||
margin-right: 6px;
|
||||
color: #222;
|
||||
margin-top: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
@ -98,8 +115,8 @@
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
#ruleModal .modal-body {
|
||||
@ -220,26 +237,140 @@
|
||||
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>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- Action Bar -->
|
||||
<div class="action-bar">
|
||||
<div class="filter-section">
|
||||
<label for="filterDepartment">Filter:</label>
|
||||
<select class="form-control" id="filterDepartment" onchange="filterRules()">
|
||||
<option value="">All Departments</option>
|
||||
<option value="Health">Health</option>
|
||||
<option value="Motor">Motor</option>
|
||||
<option value="Fire">Fire</option>
|
||||
<option value="Travel">Travel</option>
|
||||
<option value="Marine">Marine</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openModal()">Create New Rule</button>
|
||||
</div>
|
||||
<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="row">
|
||||
|
||||
<div class="form-group col-md-6">
|
||||
<input class="form-control" id="searchName" type="text" placeholder="Search rule name (partial, case-insensitive)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
<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>
|
||||
</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>
|
||||
</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 -->
|
||||
<div id="rulesList" class="rules-grid">
|
||||
<!-- Rules will be displayed here as cards -->
|
||||
@ -385,7 +516,7 @@
|
||||
<option value=">">>(greater than)</option>
|
||||
<option value="<"><(less than)</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="in">in (array)</option>
|
||||
</select>
|
||||
@ -767,7 +898,7 @@
|
||||
|
||||
function displayRules(sucess_data = null) {
|
||||
|
||||
const filter = document.getElementById('filterDepartment').value;
|
||||
// const filter = document.getElementById('filterDepartment').value ?? null;
|
||||
const container = document.getElementById('rulesList');
|
||||
|
||||
let filteredRules = sucess_data ?? rules;
|
||||
@ -787,9 +918,9 @@
|
||||
// console.log('filteredRules', filteredRules);
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
filteredRules = rules.filter(r => r.department === filter);
|
||||
}
|
||||
// if (filter) {
|
||||
// filteredRules = rules.filter(r => r.department === filter);
|
||||
// }
|
||||
|
||||
if (filteredRules.length === 0) {
|
||||
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
|
||||
displayRules();
|
||||
</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