FEAT_RULE_FILTER

This commit is contained in:
VENKATESHWARAN 2025-11-20 16:09:40 +05:30
parent fded069957
commit 528458be46

View File

@ -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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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>