nhance/app/Views/commission_rules_list.php
2025-11-20 16:09:40 +05:30

1625 lines
61 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<style>
.container {
max-width: 1400px;
margin: 0 auto;
}
.action-bar {
/* display: flex;
justify-content: space-between; */
align-items: center;
margin-bottom: 30px;
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);
}
.filter-section {
display: flex;
align-items: center;
gap: 15px;
}
/* Card Grid */
.rules-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 25px;
}
.rule-card {
background:#F5FFFF !important;
border-radius: 10px;
padding: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
transition: all 0.3s ease;
/* border-top: 3px solid #E26728; */
border-top: 3px solid #02a8b5;
}
.rule-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.rule-card-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 12px;
}
.rule-name {
font-size: 14px;
font-weight: bold;
color: #2c3e50;
margin-bottom: 4px;
line-height: 1.0;
}
.rule-details {
margin: 10px 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
line-height: 1.4;
}
.rule-actions {
display: flex;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
/* border-top: 1px solid #e9ecef; */
justify-content: flex-end;
}
.department-badge {
padding: 2px 5px;
border-radius: 15px;
font-size: 0.75rem;
font-weight: 600;
color: white;
background: linear-gradient(45deg, #3498db, #2980b9);
}
.condition-badge {
padding: 2px 5px;
border-radius: 15px;
font-size: 7px;
display: inline-block;
border-radius: 6px;
background: #f1f5f9;
margin-right: 6px;
color: #222;
margin-top: 6px;
font-weight: 600;
}
.detail-item {
font-size: 10px;
color: #555;
}
.detail-label {
font-weight: 600;
margin-right: 6px;
}
#ruleModal .modal-body {
max-height: 450px; /* adjust as needed */
overflow-y: auto;
}
.btn {
padding: 0px 10px !important;
border-radius: 18px !important;
}
.close {
color: white;
font-size: 2rem;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s;
}
.close:hover {
transform: rotate(90deg);
}
.conditions-container {
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
background: #f8f9fa;
}
.condition-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: 10px;
margin-bottom: 10px;
align-items: end;
}
.calculation-section {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 15px;
}
.component-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: 10px;
margin-bottom: 10px;
align-items: end;
}
.alert {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 600;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.hidden {
display: none;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #7f8c8d;
}
.empty-state h3 {
font-size: 1.5rem;
margin-bottom: 10px;
}
@media (max-width: 768px) {
.rules-grid {
grid-template-columns: 1fr;
}
.condition-row, .component-row {
grid-template-columns: 1fr;
}
.action-bar {
flex-direction: column;
gap: 15px;
}
.filter-section {
width: 100%;
flex-direction: column;
}
.form-control {
width: 100%;
}
}
.readonly-select {
pointer-events: none;
/* background-color: #f0f0f0; */
background-color: #e0e0e0;
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">
<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 -->
</div>
</div>
<!-- Modal -->
<div id="ruleModal" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true" aria-modal="true" data-bs-backdrop="static">
<div class="modal-dialog modal-lg" style="max-width: 800px;">
<div class="modal-content">
<div class="modal-header" style="background-color: gainsboro;">
<h5 class="modal-title" id="modalTitle">Create New Rule <span id="heading"></span></h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" onclick="closeModal()">×</button>
</div>
<div class="modal-body">
<div id="alert-container"></div>
<form id="ruleForm">
<input type="hidden" id="ruleId" name="rule_id">
<div class="form-group">
<label for="department">Department: *</label>
<select class="form-control readonly-select" id="department" name="department" required>
<?php
if (isset($departments) && count($departments)) {
foreach ($departments as $key => $value) {
echo "<option value=" . $key . ">" . $value . "</option>";
}
}
?>
</select>
</div>
<div class="form-group">
<label for="ruleName">Rule Name: *</label>
<input type="text" class="form-control" id="ruleName" name="rule_name" required placeholder="Enter rule name">
</div>
<!-- Conditions Section -->
<div class="form-group">
<label>Conditions:</label>
<div class="conditions-container">
<div id="conditionsContainer">
<!-- Conditions will be added dynamically -->
</div>
<button type="button" class="btn btn-primary btn-small" onclick="addCondition()">+ Add Condition</button>
</div>
</div>
<!-- Calculation Section -->
<div class="form-group">
<label>Calculation:</label>
<div class="calculation-section">
<div class="form-group">
<label for="calculationType">Calculation Type:</label>
<select class="form-control" id="calculationType" name="calculation_type" onchange="toggleCalculationFields()">
<option value="percentage">Percentage</option>
<option value="composite">Composite</option>
<option value="fixed">Fixed Amount</option>
</select>
</div>
<div id="simpleCalculation">
<div class="form-group">
<label for="calculationValue">Value:</label>
<input type="number" class="form-control" id="calculationValue" name="calculation_value" step="0.01">
</div>
<div class="form-group">
<label for="calculationOn">Applied On:</label>
<select class="form-control" id="calculationOn" name="calculation_on">
<option value="premium">Premium</option>
<option value="net_premium">Net Premium</option>
<option value="od_premium">OD Premium</option>
<option value="tp_premium">TP Premium</option>
</select>
</div>
</div>
<div id="compositeCalculation" class="hidden">
<div id="componentsContainer">
<!-- Composite components will be added here -->
</div>
<button type="button" class="btn btn-primary btn-small" onclick="addComponent()">+ Add Component</button>
</div>
</div>
</div>
<div class="form-group" style="display: flex; gap: 10px;">
<button type="submit" class="btn btn-success" id="save_rule_btn">Save Rule</button>
<button type="button" class="btn btn-secondary" id="rest_rule_btn" onclick="resetForm()">Reset Form</button>
<small class="text-danger" id="used_text"></small>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
let commission_file_id = '<?= $commission_file_id ?>'
let rules = '<?= isset($rules) && !empty($rules) ? json_encode($rules) : '' ?>';
// Field options based on department
const departmentFields = JSON.parse('<?= $departmentFields ?>');
let conditionCount = 0;
let componentCount = 0;
// Initialize the page
document.addEventListener('DOMContentLoaded', function() {
// loadAllRules();
addCondition(); // Add initial condition
});
function addCondition() {
conditionCount++;
const container = document.getElementById('conditionsContainer');
const department = document.getElementById('department').value;
const conditionDiv = document.createElement('div');
conditionDiv.className = 'condition-row';
conditionDiv.id = `condition-${conditionCount}`;
let fieldOptions = '<option value="">Select Field</option>';
if (department && departmentFields[department]) {
departmentFields[department].forEach(field => {
fieldOptions += `<option value="${field}">${field.replace('_', ' ').toUpperCase()}</option>`;
});
}
conditionDiv.innerHTML = `
<div>
<select class="form-control" name="condition_field[]" required>
${fieldOptions}
</select>
</div>
<div>
<select class="form-control" name="condition_operator[]" required>
<option value="">Select Operator</option>
<option value="==">==(equals)</option>
<option value="!=">=!=(not equals)</option>
<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="between">between</option>
<option value="in">in (array)</option>
</select>
</div>
<div>
<input type="text" class="form-control" name="condition_value[]" placeholder="Value" required>
</div>
<div>
<button type="button" class="btn btn-danger btn-small" onclick="removeCondition('condition-${conditionCount}')">×</button>
</div>
`;
container.appendChild(conditionDiv);
}
function removeCondition(conditionId) {
const element = document.getElementById(conditionId);
if (element) {
element.remove();
}
}
function addComponent() {
componentCount++;
const container = document.getElementById('componentsContainer');
const componentDiv = document.createElement('div');
componentDiv.className = 'component-row';
componentDiv.id = `component-${componentCount}`;
componentDiv.innerHTML = `
<div>
<input type="number" class="form-control" name="component_percentage[]" placeholder="Percentage" step="0.01" required>
</div>
<div>
<select class="form-control" name="component_on[]" required>
<option value="premium">Premium</option>
<option value="net_premium">Net Premium</option>
<option value="od_premium">OD Premium</option>
<option value="tp_premium">TP Premium</option>
</select>
</div>
<div>
<label><input type="checkbox" name="only_first_year[]" value="1"> Only First Year</label>
</div>
<div>
<button type="button" class="btn btn-danger btn-small" onclick="removeComponent('component-${componentCount}')">×</button>
</div>
`;
container.appendChild(componentDiv);
}
function removeComponent(componentId) {
const element = document.getElementById(componentId);
if (element) {
element.remove();
}
}
function toggleCalculationFields() {
const type = document.getElementById('calculationType').value;
const simple = document.getElementById('simpleCalculation');
const composite = document.getElementById('compositeCalculation');
if (type === 'composite') {
simple.classList.add('hidden');
composite.classList.remove('hidden');
if (componentCount === 0) {
addComponent();
}
} else {
simple.classList.remove('hidden');
composite.classList.add('hidden');
}
}
// Update field options when department changes
document.getElementById('department').addEventListener('change', function() {
const conditions = document.querySelectorAll('[name="condition_field[]"]');
const department = this.value;
conditions.forEach(select => {
const currentValue = select.value;
select.innerHTML = '<option value="">Select Field</option>';
if (department && departmentFields[department]) {
departmentFields[department].forEach(field => {
const option = document.createElement('option');
option.value = field;
option.textContent = field.replace('_', ' ').toUpperCase();
if (field === currentValue) {
option.selected = true;
}
select.appendChild(option);
});
}
});
});
// Form submission
document.getElementById('ruleForm').addEventListener('submit', function(e) {
e.preventDefault();
const rule_id = document.getElementById('ruleId').value || null;
const formData = new FormData(this);
const ruleData = {
id: document.getElementById('ruleId').value || null,
name: formData.get('rule_name'),
department: formData.get('department'),
conditions: [],
calculation: {}
};
// Process conditions
const fields = formData.getAll('condition_field[]');
const operators = formData.getAll('condition_operator[]');
const values = formData.getAll('condition_value[]');
for (let i = 0; i < fields.length; i++) {
if (fields[i] && operators[i] && values[i]) {
let value = values[i];
// Parse value based on operator
if (operators[i] === 'between') {
value = value.split(',').map(v => parseFloat(v.trim()));
} else if (operators[i] === 'in') {
value = value.split(',').map(v => v.trim());
} else if (!isNaN(value)) {
value = parseFloat(value);
} else if (value.toLowerCase() === 'true') {
value = true;
} else if (value.toLowerCase() === 'false') {
value = false;
}
ruleData.conditions.push({
field: fields[i],
operator: operators[i],
value: value
});
}
}
// Process calculation
const calculationType = formData.get('calculation_type');
if (calculationType === 'composite') {
const percentages = formData.getAll('component_percentage[]');
const ons = formData.getAll('component_on[]');
const firstYears = formData.getAll('only_first_year[]');
ruleData.calculation = {
type: 'composite',
components: []
};
for (let i = 0; i < percentages.length; i++) {
if (percentages[i] && ons[i]) {
const component = {
percentage: parseFloat(percentages[i]),
on: ons[i]
};
if (firstYears.includes('1')) {
component.only_first_year = true;
}
ruleData.calculation.components.push(component);
}
}
} else {
ruleData.calculation = {
type: calculationType,
value: parseFloat(formData.get('calculation_value')),
on: formData.get('calculation_on')
};
}
console.log('', ruleData);
console.log(formData.get('calculation_value'));
console.log($('#calculation_value').val());
console.log(parseFloat(formData.get('calculation_value')));
let url = '<?= base_url('commission/rules/save') ?>';
// Data to send in the AJAX request
let requestData = {
file_id: commission_file_id,
rule_id: rule_id,
rule_data: ruleData,
};
$('.loader').fadeIn();
$('.loader-mask').fadeIn();
// Send AJAX request
sendAjaxRequestForGlobal(url, 'POST', requestData, function(response) {
console.log('Data fetched successfully:', response);
console.log('response.data type', typeof response.data);
console.log('rules type', typeof rules);
if (response.status == true) {
toastr.success(response.message, 'SUCCESS');
// rules = response.data;
displayRules(response.data);
}else{
toastr.warning(response.message, 'WARNING');
}
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
resetForm()
$('.close').click();
}, function(xhr, status, error) {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
console.error('Error fetching data:', error);
console.error(xhr.responseText);
toastr.error('An error occurred while fetching the data.', 'ERROR');
});
});
function resetForm() {
document.getElementById('ruleForm').reset();
document.getElementById('ruleId').value = '';
document.getElementById('conditionsContainer').innerHTML = '';
document.getElementById('componentsContainer').innerHTML = '';
conditionCount = 0;
componentCount = 0;
addCondition();
toggleCalculationFields();
}
function openModal() {
$('#modalTitle').text('Create New Rule');
var myModal = new bootstrap.Modal(document.getElementById('ruleModal'));
myModal.show();
resetForm();
}
function closeModal() {
document.getElementById('ruleModal').style.display = 'none';
}
function editRule(id) {
checkRuleUsed('edit', id);
// Convert rules JSON to array (ONLY ONCE)
if (typeof rules === "string") {
rules = JSON.parse(rules);
}
const rule = rules.find(r => r.id == id);
if (!rule) return;
console.log({ rule });
$('#modalTitle').text('Edit Rule');
$('#ruleId').val(rule.id);
$('#department').val(rule.department).trigger('change');
$('#ruleName').val(rule.name);
// Clear existing conditions
$('#conditionsContainer').html('');
conditionCount = 0;
// Add conditions
rule.conditions.forEach(condition => {
addCondition();
const lastCondition = $('#conditionsContainer .condition-row:last');
lastCondition.find('[name="condition_field[]"]').val(condition.field);
lastCondition.find('[name="condition_operator[]"]').val(condition.operator);
lastCondition.find('[name="condition_value[]"]').val(
Array.isArray(condition.value) ? condition.value.join(', ') : condition.value
);
});
// Set calculation type
$('#calculationType').val(rule.calculation.type);
toggleCalculationFields();
// If composite calculation
if (rule.calculation.type === 'composite') {
$('#componentsContainer').html('');
componentCount = 0;
rule.calculation.components.forEach(component => {
addComponent();
const lastComponent = $('#componentsContainer .component-row:last');
lastComponent.find('[name="component_percentage[]"]').val(component.percentage);
lastComponent.find('[name="component_on[]"]').val(component.on);
if (component.only_first_year) {
lastComponent.find('[name="only_first_year[]"]').prop('checked', true);
}
});
} else {
$('#calculationValue').val(rule.calculation.value);
$('#calculationOn').val(rule.calculation.on);
}
var myModal = new bootstrap.Modal(document.getElementById('ruleModal'));
myModal.show();
}
function deleteRule(id) {
Swal.fire({
icon: "warning",
title: "Are you sure you want to delete this rule?",
showCancelButton: true, // For Cancel
confirmButtonText: "Yes",
cancelButtonText: "Cancel",
confirmButtonColor: "#ff3333"
}).then((result) => {
if (result.isConfirmed) {
let url = '<?= base_url('commission/rules/remove') ?>';
// Data to send in the AJAX request
let requestData = {
file_id: commission_file_id,
rule_id: id,
is_deleted: true,
};
$('.loader').fadeIn();
$('.loader-mask').fadeIn();
// Send AJAX request
sendAjaxRequestForGlobal(url, 'POST', requestData, function(response) {
console.log('Data fetched successfully:', response);
console.log('response.data type', typeof response.data);
console.log('rules type', typeof rules);
if (response.status == true) {
toastr.success(response.message, 'SUCCESS');
// rules = response.data;
displayRules(response.data);
}else{
toastr.warning(response.message, 'WARNING');
}
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
resetForm()
$('.close').click();
}, function(xhr, status, error) {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
console.error('Error fetching data:', error);
console.error(xhr.responseText);
toastr.error('An error occurred while fetching the data.', 'ERROR');
});
}
});
}
function filterRules() {
displayRules();
}
function displayRules(sucess_data = null) {
// const filter = document.getElementById('filterDepartment').value ?? null;
const container = document.getElementById('rulesList');
let filteredRules = sucess_data ?? rules;
// console.log('filteredRules', filteredRules);
if(filteredRules == ""){
container.innerHTML = `
<div class="empty-state" style="grid-column: 1/-1;">
<h3>📋 No rules found</h3>
</div>
`;
return;
}
if(sucess_data == null){
filteredRules = JSON.parse(filteredRules);
// console.log('filteredRules', filteredRules);
}
// if (filter) {
// filteredRules = rules.filter(r => r.department === filter);
// }
if (filteredRules.length === 0) {
container.innerHTML = `
<div class="empty-state" style="grid-column: 1/-1;">
<h3>📋 No rules found</h3>
<p>Create your first rule to get started!</p>
</div>
`;
return;
}
container.innerHTML = filteredRules.map(rule => {
// Build condition badges
const conditionBadges = rule.conditions
.map(c => `<span class="condition-badge">${c.field}</span>`)
.join(' ');
return `
<div class="rule-card">
<div class="rule-card-header">
<div>
<div class="rule-name">${rule.name}</div>
</div>
<span class="department-badge">${rule.department.charAt(0).toUpperCase() + rule.department.slice(1)}</span>
</div>
<div class="rule-details">
<div class="detail-item">
<span class="detail-label">Calculation Type:</span> ${rule.calculation.type}
</div>
<div class="detail-item">
<span class="detail-label">Value:</span> ${rule.calculation.value || "N/A"}${rule.calculation.type === 'percentage' ? '%' : ''}
</div>
<div class="detail-item">
<span class="detail-label">Applied On:</span> ${rule.calculation.on || 'N/A'}
</div>
<div class="detail-item">
<span class="detail-label">Conditions:</span> ${rule.conditions.length} condition(s)
</div>
</div>
${conditionBadges}
${rule.is_deleted
? `<div class="rule-actions">
<button class="btn btn-danger btn-small" disabled>Deleted</button>
</div>`
: `<div class="rule-actions">
<button class="btn btn-primary btn-small" onclick="editRule('${rule.id}')">
<i class="mdi mdi-pencil"></i>
</button>
<button class="btn btn-danger btn-small" onclick=" checkRuleUsed('delete','${rule.id}')">
<i class="mdi mdi-delete"></i>
</button>
</div>`
}
</div>
`;
}).join('');
}
function checkRuleUsed(type, rule_id){
let url = '<?= base_url('commission/checkRuleUsage') ?>';
// Data to send in the AJAX request
let requestData = {
rule_id: rule_id,
};
// Send AJAX request
sendAjaxRequestForGlobal(url, 'GET', requestData, function(response) {
console.log('Data fetched successfully:', response);
if (response.status == true) {
if(type == "edit"){
if (response.count > 0) {
$('#save_rule_btn').prop('disabled', true);
$('#rest_rule_btn').prop('disabled', true);
$('#used_text').text('This rule is already used and cannot be edited.');
} else {
$('#save_rule_btn').prop('disabled', false);
$('#rest_rule_btn').prop('disabled', false);
$('#used_text').text('');
}
}else{
if (response.count > 0) {
Swal.fire({
icon: "warning",
title: "This rule is already used and cannot be deleted.",
})
}else{
deleteRule(rule_id);
}
}
}
}, function(xhr, status, error) {
console.error('Error fetching data:', error);
console.error(xhr.responseText);
toastr.error('An error occurred while fetching the data.', 'ERROR');
});
}
$(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>