nhance/app/Views/faq_list.php

563 lines
25 KiB
PHP
Raw Permalink 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>
.col-12 {
max-width: 98% !important;
}
.custom-dropdown-menu {
display: none;
position: absolute;
background-color: #ffffff !important;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.25rem;
padding: 0.5rem 0;
min-width: 10rem;
z-index: 9999;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.custom-dropdown-menu .dropdown-item {
display: block !important;
width: 100% !important;
padding: 0.5rem 1rem !important;
color: #212529 !important;
text-decoration: none !important;
background-color: transparent !important;
}
.custom-dropdown-menu .dropdown-item:hover {
background-color: #f8f9fa !important;
color: #16181b !important;
cursor: pointer !important;
}
.dataTables_length label {height: 21px !important;}
</style>
<?php
helper('datatable_view');
$faq_header_labels = ['S.No.', 'Category', 'Question', 'Status', 'Action'];
$faq_col_count = count($faq_header_labels);
$faq_col_max_len = array_fill(0, $faq_col_count, 0);
for ($i = 0; $i < $faq_col_count; $i++) {
$faq_col_max_len[$i] = mb_strlen($faq_header_labels[$i]);
}
$faq_list_rows = $faq_list ?? [];
$faq_sl = 0;
foreach ($faq_list_rows as $row) {
++$faq_sl;
$faq_col_max_len[0] = max($faq_col_max_len[0], mb_strlen((string) $faq_sl));
$category = $row['category'] ? $row['category'] : 'N/A';
$catDisp = (strlen($category) > 50) ? substr($category, 0, 50) . '...' : $category;
$faq_col_max_len[1] = max($faq_col_max_len[1], mb_strlen($catDisp));
$question_text = $row['question'] ? strip_tags($row['question']) : 'N/A';
$qDisp = (strlen($question_text) > 50) ? substr($question_text, 0, 50) . '...' : $question_text;
$faq_col_max_len[2] = max($faq_col_max_len[2], mb_strlen($qDisp));
$faq_st = (isset($row['is_active']) && (int) $row['is_active'] === 1) ? 'Active' : 'In-Active';
$faq_col_max_len[3] = max($faq_col_max_len[3], mb_strlen($faq_st));
$faq_col_max_len[4] = max($faq_col_max_len[4], 4);
}
$faq_col_min_px = [48, 100, 200, 88, 72];
$faq_col_max_px = [64, 280, 480, 120, 88];
$faq_col_width_px = nhance_dt_column_widths_px($faq_header_labels, $faq_col_max_len, $faq_col_min_px, $faq_col_max_px);
?>
<style>
<?php for ($i = 0; $i < $faq_col_count; $i++) : ?>
#faq-list-table_wrapper .dataTables_scrollHead table thead th:nth-child(<?= $i + 1 ?>),
#faq-list-table thead th:nth-child(<?= $i + 1 ?>) {
min-width: <?= (int) $faq_col_width_px[$i] ?>px;
width: <?= (int) $faq_col_width_px[$i] ?>px;
box-sizing: border-box;
vertical-align: middle;
overflow: visible !important;
}
#faq-list-table tbody td:nth-child(<?= $i + 1 ?>) {
width: <?= (int) $faq_col_width_px[$i] ?>px;
max-width: <?= (int) $faq_col_width_px[$i] ?>px;
min-width: <?= (int) $faq_col_width_px[$i] ?>px;
box-sizing: border-box;
vertical-align: middle;
}
<?php endfor; ?>
#faq-list-table tbody td:nth-child(1),
#faq-list-table tbody td:nth-child(2),
#faq-list-table tbody td:nth-child(3),
#faq-list-table tbody td:nth-child(4) {
overflow: hidden;
text-overflow: ellipsis;
}
#faq-list-table tbody td:nth-child(5) {
overflow: visible;
text-overflow: clip;
}
#faq-list-table tbody td {
padding: 5px 11px !important;
font-size: 13px;
line-height: 1.25;
}
#faq-list-table thead th {
padding: 5px 2.35rem 5px 11px !important;
font-size: 13px;
line-height: 1.25;
}
</style>
<!-- End ADD and EDIT Page HTML -->
<div class="row" id="List-page">
<div class="col-12">
<div class="card">
<div class="card-body">
<table data-custom-table-css="table" class="table mb-0 nowrap" cellspacing="0" id="faq-list-table">
<thead class="bg-light">
<tr>
<th class="font-weight-medium">S.No.&nbsp;</th>
<th class="font-weight-medium">Category&nbsp;</th>
<th class="font-weight-medium">Question&nbsp;</th>
<!-- <th class="font-weight-medium">Answer</th> -->
<th class="font-weight-medium">Status&nbsp;</th>
<th class="font-weight-medium text-center">Action&nbsp;</th>
</tr>
</thead>
<tbody>
<?php if(!empty($faq_list)) {
// <?php if(isset($faq_list)) {
$slno = 1; ?>
<?php foreach($faq_list as $index => $row) { ?>
<tr>
<td><?= $slno++; ?></td>
<td><?php $category = $row['category'] ? $row['category'] : 'N/A';
echo (strlen($category) > 50)
? htmlspecialchars(substr($category, 0, 50)) . "..."
: htmlspecialchars($category);
?></td>
<td><?php
$question_text = $row['question'] ? strip_tags($row['question']) : 'N/A';
echo (strlen($question_text) > 50)
? htmlspecialchars(substr($question_text, 0, 50)) . "..."
: htmlspecialchars($question_text);
?></td>
<!-- <td><?php $answer = $row['answer'] ? $row['answer'] : 'N/A';
echo (strlen($answer) > 50)
? htmlspecialchars(substr($answer, 0, 50)) . "..."
: htmlspecialchars($answer);
?></td> -->
<td>
<span class="<?php if($row['is_active'] == 1){ echo 'badge badge-primary'; }else{ echo 'badge badge-danger'; } ?>">
<?php if($row['is_active'] == 1){ echo 'Active'; }else{ echo 'In-Active'; } ?>
</span>
</td>
<td class="text-center table-action-cell">
<div class="btn-group dropdown">
<a href="javascript: void(0);" class="dropdown-toggle arrow-none btn btn-light btn-sm" data-toggle="dropdown" aria-expanded="false"><i class="mdi mdi-dots-horizontal"></i></a>
<div class="dropdown-menu dropdown-menu-right" style="cursor: pointer;">
<a class="dropdown-item" data-id="<?= $row['id']; ?>" onclick="handleSaveEditAndDelete('edit', this)">
<i class="mdi mdi-pencil mr-2 text-muted font-18 vertical-middle"></i>Edit
</a>
<?php if ($row['is_active'] == 1): ?>
<a class="dropdown-item" data-id="<?= $row['id']; ?>" onclick="handleSaveEditAndDelete('remove', this)">
<i class="mdi mdi-delete mr-2 text-muted font-18 vertical-middle"></i>Delete
</a>
<?php endif; ?>
</div>
</div>
</td>
</tr>
<?php } } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- modal content -->
<div id="con-close-modal" class="modal fade app-font-family" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" style="display: none;" data-backdrop="static">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="modalLabel">Add FAQ Details</h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
</div>
<div class="modal-body p-4">
<form class="parsley-examples" id="FAQForm" enctype="multipart/form-data">
<input type="hidden" name="faq_id" id="faq_id"/>
<div class="form-group">
<div class="form-row">
<div class="form-group col-md-12">
<label for="category">Category<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="category" name="category" placeholder="Enter Category" required
data-parsley-pattern="^[a-zA-Z0-9 \/\-\.\,&quot;\']+$"
data-parsley-pattern-message="Category can contain only letters, numbers, spaces, and these characters: / - . , &quot; '"
data-parsley-maxlength="100"
data-parsley-maxlength-message="Category cannot exceed 100 characters"
data-parsley-trigger="keyup input">
<small class="form-text text-muted text-end text-right">
Please enter the sub-category separated by '/'. For Example: Category / Category-1 / Category-2
</small>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="question">Question<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="question" name="question" placeholder="Enter Question" required
data-parsley-pattern="^[a-zA-Z0-9 _\-\.,;:!?()&\/]+$"
data-parsley-pattern-message="Question can contain only letters, numbers, spaces, and these characters: . , ; : ! ? ( ) & / -"
data-parsley-maxlength="1000"
data-parsley-maxlength-message="Question cannot exceed 1000 characters"
data-parsley-trigger="keyup input">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="answer">Answer<span class="text-danger">*</span></label>
<!-- <textarea id="answer" name="answer" class="form-control" required></textarea> -->
<textarea id="answer" name="answer" class="form-control answer" rows="7" required
data-parsley-maxlength="5000"
data-parsley-maxlength-message="Answer cannot exceed 5000 characters">
<!-- <h5>Hello {USER_NAME}, </h5>
<p>We create simple, flat & responsive custom mail template.</p>
<p>Please, write text here!</p> -->
</textarea>
<div id="answer-errors"></div>
</div>
</div>
</div>
<div class="form-group text-right m-b-0">
<button type="submit" class="btn app-btn-secondary" id="btnSubmit">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- <script src="https://cdn.ckeditor.com/ckeditor5/39.0.1/classic/ckeditor.js"></script> -->
<script>
// let faqEditor;
let editor;
// ClassicEditor
// .create(document.querySelector('#answer'))
// .then(editor => {
// faqEditor = editor; // Save the instance here
// })
// .catch(error => { console.error(error); });
var table;
$(document).ready(function () {
//JoDit editor
const editorConfig = {
buttons: [
"undo", "redo", "|",
"paragraph",
"bold", "italic", "strikethrough", "|",
"superscript", "subscript", "|",
"ul", "ol", "|",
"outdent", "indent", "|",
"blockquote", "table", "|",
"align", "fontsize", "|",
"source"
],
controls: {
paragraph: {
list: {
p: "Normal",
h1: "Heading 1",
h2: "Heading 2",
h3: "Heading 3",
h4: "Heading 4"
}
}
},
fontsize: ["8px", "10px", "12px", "14px", "16px", "18px", "20px", "22px", "24px"], // Font sizes in pixels
showPlaceholder: false,
toolbarButtonSize: "small",
toolbarAdaptive: false,
saveHeightInStorage: true,
minHeight: 400,
height: 400, // Optional fixed height
defaultMode: "1", // Start in WYSIWYG mode
enableDragAndDropFileToEditor: true, // Allow file uploads via drag and drop
placeholder: "Start typing here...", // Optional placeholder text
};
// 2. Initialize
editor = Jodit.make("#answer", editorConfig);
editor.events.on('change keyup', function () {
$('#answer').val(editor.value);
$('#answer').parsley().validate();
});
var ticketsTable = $('#faq-list-table');
nhanceListDataTableBeforeInit();
var table = ticketsTable.DataTable(nhanceMergeListDataTableOptions({
// dom: "<'row'<'col-sm-7'f><'col-sm-5 text-right'B>>" + // Filter left, buttons right
// "<'row'<'col-sm-12'tr>>" +
// "<'row'<'col-sm-5'i><'col-sm-7'p>>", // Pagination at the bottom, without page length selector
dom: "<'row'<'col-sm-7'f><'col-sm-5 text-right'B>>" + // Filter left, buttons right
"<'row'<'col-sm-12'tr>>" +
"<'row align-items-center'<'col-5 text-start'i><'col-7 d-flex justify-content-end align-items-center'<'me-2'l>p>>",
lengthMenu: [[10, 20, 50, 100], [10, 20, 50, 100]],
buttons: [
{
text: '<i class="mdi mdi-plus"></i><span class="btn-custom"> Add </span>',
action: function(e, dt, node, config) {
resetValues();openModal();
}
},
{
extend: 'collection',
text: '<span class=" btn-custom"> Export </span><i class="mdi mdi-menu-down"></i>',
className: 'btn app-btn-secondary ',
buttons: [
{
extend: 'csv',
text: '<i class="mdi mdi-file-delimited " ></i><span class=" btn-custom"> CSV </span>',
title: 'FAQ-List',
className: 'app-btn-primary ',
exportOptions: {
columns: ':not(:last-child)'
},
}
]
}
],
language: {
search: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666; display:none;"></i>
</div>`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>',
paginate: {
previous: '◄',
next: '►'
}
},
paging: true, // Enable pagination
pageLength: 10, // Set default number of rows per page (optional)
// ordering: false,
columnDefs: [
<?php for ($i = 0; $i < $faq_col_count; $i++) : ?>
{ targets: <?= $i ?>, width: '<?= (int) $faq_col_width_px[$i] ?>px' },
<?php endfor; ?>
],
}));
nhanceListDataTableAfterInit();
nhanceListDataTableBindAdjust(table);
function placeActionDropdown($toggle) {
const $group = $toggle.closest('.btn-group.dropdown');
const $menu = $group.find('.dropdown-menu');
if (!$group.length || !$menu.length) return;
// Reset previous state before recalculating placement.
$group.removeClass('dropup');
const toggleRect = $toggle[0].getBoundingClientRect();
const menuHeight = $menu.outerHeight() || 120;
const spaceBelow = window.innerHeight - toggleRect.bottom;
const spaceAbove = toggleRect.top;
// Open upward only when bottom space is insufficient.
if (spaceBelow < menuHeight + 12 && spaceAbove > menuHeight) {
$group.addClass('dropup');
}
}
$('#faq-list-table').on('show.bs.dropdown', '.table-action-cell .dropdown-toggle', function () {
const $toggle = $(this);
const $scrollBody = $toggle.closest('.dataTables_scrollBody');
if ($scrollBody.length) {
$scrollBody.data('prev-overflow-y', $scrollBody.css('overflow-y'));
$scrollBody.css('overflow-y', 'visible');
}
placeActionDropdown($toggle);
});
$('#faq-list-table').on('hide.bs.dropdown', '.table-action-cell .dropdown-toggle', function () {
const $scrollBody = $(this).closest('.dataTables_scrollBody');
const prevOverflowY = $scrollBody.data('prev-overflow-y');
if ($scrollBody.length) {
$scrollBody.css('overflow-y', prevOverflowY || 'auto');
}
});
ticketsTable.on('draw.dt', function () {
$('#faq-list-table tbody tr td.table-action-cell .btn-group.dropdown').removeClass('dropup');
});
});
$('.close').click(function(){ resetValues(); })
function openModal(){
var myModal = new bootstrap.Modal(document.getElementById('con-close-modal'));
myModal.show();
}
function handleSaveEditAndDelete(type = 'submit', el = null) {
let form = document.getElementById('FAQForm');
let url = '<?= base_url('FAQ') ?>';
let method = "POST";
let faq_id = el ? $(el).data('id') : null;
let formData;
if (type === 'submit') {
// CREATE FORMDATA FROM FORM
formData = new FormData(form);
formData.set('return_type', 'web');
} else if (type == 'edit') {
method = "GET";
if (faq_id) url += '?faq_id=' + faq_id ;
$('#modalLabel').text('Edit FAQ Details');
$('#FAQForm')[0].reset();
} else if (type == 'remove') {
method = "DELETE";
if (faq_id) url += '?faq_id=' + faq_id ;
}
$('.loader, .loader-mask').fadeIn();
$.ajax({
url: url,
type: method,
data: (type === 'submit') ? formData : null,
processData: false,
contentType: false,
dataType: 'json',
success: function (response) {
console.log('Response:', response);
if (response.status === 'success' || response.status === true) {
if (type == 'edit') {
let record = response.data.faq_list[0]; // single record
appendEditData(record);
} else if(type == 'remove'){
toastr.success(response.message, 'Success');
// if(el) $(el).closest('tr').remove();
window.location.reload();
// window.location.href = "<?= base_url('FAQ?return_type=web') ?>";
} else if(type == 'submit'){
toastr.success(response.message, 'Success');
var myModal = new bootstrap.Modal(document.getElementById('con-close-modal'));
myModal.hide();
$('#FAQForm')[0].reset();
window.location.reload();
}
} else {
toastr.warning(response.message, 'Warning');
}
$('.loader, .loader-mask').fadeOut();
},
error: function (xhr, status, error) {
$('.loader, .loader-mask').fadeOut();
if (xhr.status === 400) {
let response = JSON.parse(xhr.responseText);
let errorMessages = "";
let seenMessages = []; // Array to store unique messages
if (response.errors) {
$.each(response.errors, function (field, message) {
if (!seenMessages.includes(message)) {
errorMessages += `• ${message}<br>`;
seenMessages.push(message); // Mark this message as "seen"
}
});
toastr.error(errorMessages, 'Validation Error', { "allowHtml": true });
} else {
toastr.warning(response.message || 'Validation failed', 'Warning');
}
} else if (xhr.status === 403) {
let response = JSON.parse(xhr.responseText);
toastr.error(response.message, 'Security Policy');
} else if (xhr.status === 500) {
toastr.error('Something went wrong . Please try again later.', 'Server Error');
} else {
toastr.error('An unexpected error occurred. Please try again later.', 'Error');
}
}
});
}
function resetValues(){
$('#modalLabel').text('Add FAQ Details');
$('#FAQForm')[0].reset();
if (typeof editor !== 'undefined') {
editor.value = '';
}
}
function appendEditData(data) {
$('#FAQForm')[0].reset();
$('#faq_id').val(data.id);
$('#category').val(data.category);
// Safely set the question value by stripping any HTML tags
$('#question').val($('<div/>').html(data.question || '').text());
if (editor) {
editor.value = data.answer || '';
}
openModal();
}
// Simple client-side XSS pattern check to give instant feedback
function hasUnsafeHtml(html) {
if (!html) return false;
const decoded = $('<textarea/>').html(html).text().toLowerCase();
const patterns = [
/<\s*script/i,
/on\w+\s*=/i,
/javascript\s*:/i,
/vbscript\s*:/i,
/data\s*:\s*text\/html/i,
/expression\s*\(/i
];
return patterns.some(p => p.test(decoded));
}
$('#FAQForm').on('submit', function(e) {
e.preventDefault();
const form = this;
// HTML5 built-in validation
if (!form.checkValidity()) {
form.reportValidity(); // shows required/pattern tooltips
return;
}
var isValid = $(this).parsley({
excluded: 'input[type=button], input[type=submit], input[type=reset], input[type=hidden]'
}).validate();
if (!isValid) return;
// If valid, submit via AJAX
handleSaveEditAndDelete('submit');
});
// Restrict user typing to allow Regex only characters
$('#category').on('input', function() {
this.value = this.value.replace(/[^a-zA-Z0-9 \/\-.,"']/g, '');
});
$('#question').on('input', function() {
this.value = this.value.replace(/[^a-zA-Z0-9 _\-.,;:!?()&\/]/g, '');
});
</script>