bug fix in clients controller
This commit is contained in:
416
public/assets/js/client-index.js
Normal file
416
public/assets/js/client-index.js
Normal file
@@ -0,0 +1,416 @@
|
||||
// public/js/client-index.js
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#clientService').select2({
|
||||
placeholder: "-- Select Services --",
|
||||
allowClear: true,
|
||||
dropdownParent: $('#createClientModal')
|
||||
});
|
||||
let searchTimer;
|
||||
|
||||
// Initialize the table on page load
|
||||
fetchClients();
|
||||
|
||||
// Event Listeners for Search and Filters
|
||||
$('#searchClient').on('keyup', function() {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => fetchClients(1), 400);
|
||||
});
|
||||
|
||||
$('#filterService, #filterBilling').on('change', function() {
|
||||
fetchClients(1);
|
||||
});
|
||||
$('#filterService, #filterStatus').on('change', function() {
|
||||
fetchClients(1);
|
||||
});
|
||||
|
||||
// Handle Pagination Clicks dynamically
|
||||
$(document).on('click', '.page-link-ajax', function(e) {
|
||||
e.preventDefault();
|
||||
let page = $(this).data('page');
|
||||
if (page) fetchClients(page);
|
||||
});
|
||||
|
||||
$('#btnOpenClientModal').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('#createClientModal').modal('show');
|
||||
});
|
||||
|
||||
$(document).on('click', '.btn-view-client', function(e) {
|
||||
e.preventDefault();
|
||||
let clientId = $(this).data('id');
|
||||
window.location.href = base_url + "/clients/" + clientId;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// EDIT CLIENT MODAL TRIGGER & POPULATION
|
||||
// ---------------------------------------------------------
|
||||
$(document).on('click', '.btn-edit-client', function(e) {
|
||||
e.preventDefault();
|
||||
let clientId = $(this).data('id');
|
||||
let $form = $('#editClientForm');
|
||||
|
||||
// Set dynamic action URL for update
|
||||
$form.attr('action', base_url + '/clients/' + clientId);
|
||||
|
||||
// Fetch Services first, then Client data to populate the edit modal
|
||||
$.ajax({
|
||||
url: base_url + '/api/services',
|
||||
type: 'GET',
|
||||
success: function(services) {
|
||||
let $servicesSelect = $('#edit_services');
|
||||
$servicesSelect.empty();
|
||||
if (services && Array.isArray(services)) {
|
||||
services.forEach(function(s) {
|
||||
$servicesSelect.append(new Option(s.name, s.id));
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch client details JSON
|
||||
$.ajax({
|
||||
url: base_url + '/clients/' + clientId + '/json',
|
||||
type: 'GET',
|
||||
success: function(client) {
|
||||
$('#edit_name').val(client.name);
|
||||
$('#edit_email').val(client.email);
|
||||
$('#edit_phone').val(client.phone);
|
||||
$('#edit_contact_person').val(client.contact_person);
|
||||
$('#edit_company_type').val(client.company_type);
|
||||
$('#edit_contract_type').val(client.contract_type);
|
||||
$('#edit_industry').val(client.industry);
|
||||
$('#edit_status').val(client.status);
|
||||
$('#edit_currency').val(client.currency);
|
||||
// $('#edit_country').val(client.country);
|
||||
loadCountries(client.country);
|
||||
// Helper for multi-select Select2 fields
|
||||
function setSelect2Values(selector, values) {
|
||||
let $el = $(selector);
|
||||
$el.val(null).trigger('change');
|
||||
if (values && Array.isArray(values)) {
|
||||
values.forEach(function(val) {
|
||||
if ($el.find("option[value='" + val + "']").length === 0) {
|
||||
$el.append(new Option(val, val, true, true));
|
||||
}
|
||||
});
|
||||
$el.val(values).trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse services if stored as JSON string
|
||||
let clientServices = client.services;
|
||||
if (typeof clientServices === 'string') {
|
||||
try { clientServices = JSON.parse(clientServices); } catch(err) {}
|
||||
}
|
||||
|
||||
setSelect2Values('#edit_services', clientServices);
|
||||
setSelect2Values('#edit_message_types', client.message_types);
|
||||
setSelect2Values('#edit_connections', client.connections);
|
||||
setSelect2Values('#edit_support_phones', client.support_phones);
|
||||
setSelect2Values('#edit_support_emails', client.support_emails);
|
||||
setSelect2Values('#edit_rate_emails', client.rate_emails);
|
||||
setSelect2Values('#edit_support_skype', client.support_skype);
|
||||
|
||||
// Show the modal after populating
|
||||
$('#editClientModal select[multiple]').each(function() {
|
||||
if (!$(this).hasClass("select2-hidden-accessible")) {
|
||||
$(this).select2({
|
||||
theme: 'bootstrap-5',
|
||||
dropdownParent: $('#editClientModal'),
|
||||
tags: true, // Allows typing custom entries for emails, phones, etc.
|
||||
tokenSeparators: [',', ' '],
|
||||
placeholder: 'Select or type and hit enter...'
|
||||
});
|
||||
}
|
||||
});
|
||||
$('#editClientModal').modal('show');
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire('Error', 'Could not fetch client details.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#createClientModal, #editClientModal').on('hidden.bs.modal', function () {
|
||||
let $form = $(this).find('form');
|
||||
if ($form.length) {
|
||||
$form[0].reset();
|
||||
// Reset any select elements or select2 tags back to blank
|
||||
$form.find('select').val(null).trigger('change');
|
||||
}
|
||||
});
|
||||
// ---------------------------------------------------------
|
||||
// AJAX FORM SUBMISSION (Create Client)
|
||||
// ---------------------------------------------------------
|
||||
$('#createClientForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let $form = $(this);
|
||||
let $submitBtn = $('#btnSubmitClient');
|
||||
let originalText = $submitBtn.html();
|
||||
let $alertBox = $('#clientModalAlert');
|
||||
|
||||
$submitBtn.html('<span class="spinner-border spinner-border-sm me-2"></span>Saving...').prop('disabled', true);
|
||||
$alertBox.html('');
|
||||
$form.find('.is-invalid').removeClass('is-invalid');
|
||||
$form.find('.invalid-feedback').remove();
|
||||
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
method: 'POST',
|
||||
data: $form.serialize(),
|
||||
success: function(response) {
|
||||
if(response.success) {
|
||||
$alertBox.html(`
|
||||
<div class="alert alert-success d-flex align-items-center" role="alert">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
<div>${response.message || 'Client added successfully!'}</div>
|
||||
</div>
|
||||
`);
|
||||
fetchClients(1);
|
||||
setTimeout(() => {
|
||||
$('#createClientModal').modal('hide');
|
||||
$form[0].reset();
|
||||
$alertBox.html('');
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 401) {
|
||||
window.location.href = base_url + '/login';
|
||||
}
|
||||
if (xhr.status === 422) {
|
||||
let errors = xhr.responseJSON.errors;
|
||||
$alertBox.html(`
|
||||
<div class="alert alert-danger d-flex align-items-center" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div>Please fix the errors highlighted below.</div>
|
||||
</div>
|
||||
`);
|
||||
$.each(errors, function(key, value) {
|
||||
let fieldName = key;
|
||||
if(key === 'services') fieldName = 'services[]';
|
||||
let $input = $form.find('[name="' + fieldName + '"]');
|
||||
if ($input.length) {
|
||||
$input.addClass('is-invalid');
|
||||
$input.parent().append('<div class="invalid-feedback d-block fw-medium">' + value[0] + '</div>');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$alertBox.html(`
|
||||
<div class="alert alert-danger d-flex align-items-center" role="alert">
|
||||
<i class="bi bi-x-circle-fill me-2"></i>
|
||||
<div>An unexpected server error occurred. Please try again.</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$submitBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#createClientModal').on('hidden.bs.modal', function () {
|
||||
$('#createClientForm')[0].reset();
|
||||
$('#clientService').val(null).trigger('change');
|
||||
$('#clientModalAlert').html('');
|
||||
$('#createClientForm').find('.is-invalid').removeClass('is-invalid');
|
||||
$('#createClientForm').find('.invalid-feedback').remove();
|
||||
});
|
||||
|
||||
// AJAX Fetch Function
|
||||
function fetchClients(page = 1) {
|
||||
const search = $('#searchClient').val();
|
||||
const service = $('#filterService').val();
|
||||
const billing = $('#filterBilling').val();
|
||||
const status = $('#filterStatus').val();
|
||||
|
||||
$('#clientTableBody').html(`
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status" style="color: #5c4df0 !important;"></div>
|
||||
<div class="mt-2 text-secondary" style="font-size: 0.85rem;">Loading clients...</div>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
|
||||
$.ajax({
|
||||
url: base_url + "/clients/data",
|
||||
type: "GET",
|
||||
data: { page: page, search: search, service: service, billing: billing, status: status },
|
||||
success: function(response) {
|
||||
renderTable(response.data);
|
||||
renderPagination(response);
|
||||
},
|
||||
error: function() {
|
||||
$('#clientTableBody').html('<tr><td colspan="7" class="text-center text-danger py-4">Failed to load data. Please try again.</td></tr>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Export Actions
|
||||
$('.btn-export').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
const search = $('#searchClient').val();
|
||||
const service = $('#filterService').val();
|
||||
const billing = $('#filterBilling').val();
|
||||
const status = $('#filterStatus').val();
|
||||
const format = $(this).data('format');
|
||||
|
||||
const queryParams = $.param({
|
||||
search: search,
|
||||
service: service,
|
||||
billing: billing,
|
||||
status: status,
|
||||
format: format
|
||||
});
|
||||
|
||||
window.location.href = base_url + "/clients/export?" + queryParams;
|
||||
});
|
||||
|
||||
// Render Table Rows
|
||||
function renderTable(clients) {
|
||||
let html = '';
|
||||
|
||||
if (clients.length === 0) {
|
||||
$('#clientTableBody').html('<tr><td colspan="7" class="text-center text-secondary py-4">No clients found matching your criteria.</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
clients.forEach(client => {
|
||||
let initials = client.name.substring(0, 2).toUpperCase();
|
||||
|
||||
let servicesHtml = '<span class="text-secondary" style="font-size: 0.8rem;">N/A</span>';
|
||||
if (client.services) {
|
||||
try {
|
||||
let servicesArray = typeof client.services === 'string' ? JSON.parse(client.services) : client.services;
|
||||
if (Array.isArray(servicesArray) && servicesArray.length > 0) {
|
||||
servicesHtml = servicesArray.map(service =>
|
||||
`<span class="badge bg-secondary bg-opacity-10 text-secondary border me-1 mb-1" style="font-size: 0.7rem;">${service}</span>`
|
||||
).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not parse services for client: " + client.name);
|
||||
}
|
||||
}
|
||||
|
||||
let statusBadge = client.status === 'Live' || client.status === 'active'
|
||||
? '<span class="badge bg-success bg-opacity-10 text-success px-2 py-1"><i class="bi bi-check-circle me-1"></i>Active</span>'
|
||||
: '<span class="badge bg-warning bg-opacity-10 text-warning px-2 py-1"><i class="bi bi-clock me-1"></i>' + client.status + '</span>';
|
||||
|
||||
let formattedDate = 'N/A';
|
||||
if (client.created_at) {
|
||||
const dateObj = new Date(client.created_at);
|
||||
formattedDate = dateObj.toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Extract Account Manager name safely
|
||||
let amName = 'Unassigned';
|
||||
if (client.account_manager && client.account_manager.name) {
|
||||
amName = client.account_manager.name;
|
||||
}
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-circle bg-primary bg-opacity-10 text-primary me-3">${initials}</div>
|
||||
<div>
|
||||
<div class="fw-bold text-dark">${client.name}<div>
|
||||
<div class="text-secondary" style="font-size: 0.8rem;"><i class="bi bi-geo-alt me-1"></i>${client.country || 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">${client.contact_person || 'N/A'}</div>
|
||||
<td>
|
||||
<span class="text-dark fw-medium" style="font-size: 0.85rem;">
|
||||
<i class="bi bi-person-badge text-secondary me-1"></i>${amName}
|
||||
</span>
|
||||
</td>
|
||||
<div class="text-secondary" style="font-size: 0.8rem;">${client.email || 'N/A'}</div>
|
||||
</td>
|
||||
<td style="max-width: 200px; white-space: normal;">
|
||||
${servicesHtml}
|
||||
</td>
|
||||
<td><span class="badge bg-info bg-opacity-10 text-info text-uppercase">${client.pay_mode || 'N/A'}</span></td>
|
||||
<td><span class="fw-medium text-primary">${formattedDate}</span></td>
|
||||
<td>${statusBadge}</td>
|
||||
<td class="text-end">
|
||||
<div class="d-flex gap-1 justify-content-end align-items-center">
|
||||
<button class="btn btn-sm btn-light text-primary me-1 btn-view-client" data-id="${client.id}" title="View Details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-light text-primary btn-edit-client" data-id="${client.id}" title="Edit Client">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#clientTableBody').html(html);
|
||||
}
|
||||
|
||||
// Render Pagination Links
|
||||
function renderPagination(response) {
|
||||
$('#paginationInfo').text(`Showing ${response.from || 0} to ${response.to || 0} of ${response.total} entries`);
|
||||
|
||||
let paginationHtml = '';
|
||||
if (response.last_page > 1) {
|
||||
let prevDisabled = response.current_page === 1 ? 'disabled' : '';
|
||||
paginationHtml += `<li class="page-item ${prevDisabled}"><a class="page-link page-link-ajax" href="#" data-page="${response.current_page - 1}">Previous</a></li>`;
|
||||
|
||||
for (let i = 1; i <= response.last_page; i++) {
|
||||
let activeClass = response.current_page === i ? 'active' : '';
|
||||
let style = response.current_page === i ? 'style="background-color: #5c4df0; border-color: #5c4df0;"' : 'class="page-link text-dark"';
|
||||
paginationHtml += `<li class="page-item ${activeClass}"><a class="page-link page-link-ajax" href="#" ${style} data-page="${i}">${i}</a></li>`;
|
||||
}
|
||||
|
||||
let nextDisabled = response.current_page === response.last_page ? 'disabled' : '';
|
||||
paginationHtml += `<li class="page-item ${nextDisabled}"><a class="page-link page-link-ajax" href="#" data-page="${response.current_page + 1}">Next</a></li>`;
|
||||
}
|
||||
|
||||
$('#paginationLinks').html(paginationHtml);
|
||||
}
|
||||
|
||||
// Helper function to populate country dropdowns dynamically
|
||||
function loadCountries(selectedCountry = '') {
|
||||
$.ajax({
|
||||
url: base_url + '/api/countries',
|
||||
type: 'GET',
|
||||
success: function(countries) {
|
||||
// Target both create and edit country dropdowns if they exist
|
||||
let $selectors = $('#create_country, #edit_country');
|
||||
|
||||
$selectors.each(function() {
|
||||
let $select = $(this);
|
||||
let currentVal = $select.val() || selectedCountry;
|
||||
|
||||
$select.empty().append('<option value="">Select Country</option>');
|
||||
|
||||
if (countries && Array.isArray(countries)) {
|
||||
countries.forEach(function(c) {
|
||||
$select.append(new Option(c.en_short_name, c.en_short_name));
|
||||
});
|
||||
}
|
||||
|
||||
if (currentVal) {
|
||||
$select.val(currentVal);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Call loadCountries on page load so it's ready for the create modal
|
||||
loadCountries();
|
||||
});
|
||||
@@ -343,7 +343,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
type: 'POST', // Spoofed as PUT via hidden input
|
||||
type: 'POST',
|
||||
data: $form.serialize(),
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
|
||||
BIN
public/favicon/android-chrome-192x192.png
Normal file
BIN
public/favicon/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
BIN
public/favicon/android-chrome-512x512.png
Normal file
BIN
public/favicon/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
public/favicon/apple-touch-icon.png
Normal file
BIN
public/favicon/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/favicon/favicon-16x16.png
Normal file
BIN
public/favicon/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 350 B |
BIN
public/favicon/favicon-32x32.png
Normal file
BIN
public/favicon/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 728 B |
BIN
public/favicon/favicon.ico
Normal file
BIN
public/favicon/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
public/favicon/site.webmanifest
Normal file
1
public/favicon/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
Reference in New Issue
Block a user