1625 lines
61 KiB
PHP
1625 lines
61 KiB
PHP
<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 ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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>
|
||
|