Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev
This commit is contained in:
commit
7a97ced0f6
@ -32,6 +32,8 @@ use App\Models\OccupancyMasterModel;
|
||||
use App\Models\LeadFilesModel;
|
||||
use App\Models\LeadInstallmentPaymentDetails;
|
||||
use App\Models\GmailSentHistoryModel;
|
||||
use App\Models\SalesActualLeadModel;
|
||||
use App\Models\SalesContactPersonModel;
|
||||
|
||||
use App\Helpers\MailHelper;
|
||||
use App\Helpers\ExcelMergeHelper;
|
||||
@ -83,6 +85,8 @@ class LeadsController extends BaseController
|
||||
protected $buisnessType;
|
||||
protected $member_data_excel_columns;
|
||||
protected $general_relationships;
|
||||
protected $leadModel;
|
||||
protected $contactModel;
|
||||
|
||||
|
||||
public function __construct()
|
||||
@ -107,6 +111,8 @@ class LeadsController extends BaseController
|
||||
$this->leadFilesModel = new LeadFilesModel();
|
||||
$this->leadInstallmentPaymentDetails = new LeadInstallmentPaymentDetails();
|
||||
$this->gmailSentHistoryModel = new GmailSentHistoryModel();
|
||||
$this->leadModel = new SalesActualLeadModel();
|
||||
$this->contactModel = new SalesContactPersonModel();
|
||||
|
||||
$this->issuer = [1 => 'JIBS', 2 => 'Nhance'];
|
||||
$this->clientType = [1 => 'Group', 2 => 'Individual'];
|
||||
@ -627,6 +633,7 @@ class LeadsController extends BaseController
|
||||
{
|
||||
|
||||
$request_data = $this->request->getPost();
|
||||
print_r($request_data);die;
|
||||
$data = sanitizeInputArrayAdvanced($request_data);
|
||||
|
||||
$data['client_type'] = 1;
|
||||
@ -825,6 +832,7 @@ class LeadsController extends BaseController
|
||||
$last_3_years_claims = $data['finyear'];
|
||||
|
||||
$processedData[] = [
|
||||
'lost_reason' => $data['lost_reason'] ?? null,
|
||||
'actual_lead_id' => $data['actual_lead_id'] ?? null,
|
||||
'lead_type' => $data['lead_type'],
|
||||
'issuer' => $data['issuer'],
|
||||
@ -4372,6 +4380,28 @@ class LeadsController extends BaseController
|
||||
'actual_lead_id' => $actual_lead_id,
|
||||
];
|
||||
|
||||
if ($actual_lead_id > 0) {
|
||||
|
||||
// 🔹 Client Details (Single Row)
|
||||
$data['actual_lead_client_details'] = $this->leadModel
|
||||
->select('company_name, email, phone, address, website, gst_number, status, assigned_to')
|
||||
->where('lead_id', $actual_lead_id)
|
||||
->first(); // first row only
|
||||
|
||||
|
||||
// 🔹 Contact Person Details (First Row Only)
|
||||
$data['actual_lead_contact_person_details'] = $this->contactModel
|
||||
->select('contact_id, name, mobile, designation, email, is_primary')
|
||||
->where('lead_id', $actual_lead_id)
|
||||
->where('is_primary',1)
|
||||
->orderBy('is_primary', 'DESC') // optional (primary first)
|
||||
->first(); // only first row
|
||||
}
|
||||
else {
|
||||
$data['actual_lead_client_details'] = null;
|
||||
$data['actual_lead_contact_person_details'] = null;
|
||||
}
|
||||
|
||||
// Fetch sales team members who are active in team 5
|
||||
$data['salse_team'] = $this->userModel
|
||||
->select('user_profiles.*')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -47,6 +47,7 @@ class LeadsModel extends Model
|
||||
'proposel_data',
|
||||
'status',
|
||||
'notes',
|
||||
'lost_reason',
|
||||
'created_at',
|
||||
'created_by',
|
||||
'updated_at',
|
||||
|
||||
@ -25,6 +25,7 @@ class SalesActivityModel extends Model
|
||||
'completion_notes',
|
||||
'completed_date',
|
||||
'parent_activity_id',
|
||||
'additional_assigned_ids',
|
||||
'created_by',
|
||||
'updated_by'
|
||||
];
|
||||
@ -64,8 +65,14 @@ class SalesActivityModel extends Model
|
||||
*/
|
||||
public function getActivitiesByLead($leadId, $status = null)
|
||||
{
|
||||
$builder = $this->select('sales_activities.*, user_profiles.first_name as assigned_to_name')
|
||||
->join('user_profiles', 'user_profiles.id = sales_activities.assigned_to', 'left')
|
||||
// Convert the INT id to a string, then wrap it in JSON quotes to match ["5", "11"]
|
||||
$subQuery = "(SELECT GROUP_CONCAT(up2.first_name SEPARATOR ', ')
|
||||
FROM user_profiles up2
|
||||
WHERE JSON_CONTAINS(sales_activities.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR)))
|
||||
) as additional_assigned_names";
|
||||
|
||||
$builder = $this->select("sales_activities.*, up1.first_name as assigned_to_name, $subQuery")
|
||||
->join('user_profiles as up1', 'up1.id = sales_activities.assigned_to', 'left')
|
||||
->where('sales_activities.lead_id', $leadId);
|
||||
|
||||
if ($status) {
|
||||
@ -78,7 +85,7 @@ class SalesActivityModel extends Model
|
||||
/**
|
||||
* Get all sales_activities with filters
|
||||
*/
|
||||
public function getActivitiesWithFilters($filters = [], $limit = 10, $offset = 0)
|
||||
public function getActivitiesWithFiltersOLD($filters = [], $limit = 10, $offset = 0)
|
||||
{
|
||||
$this->select('sales_activities.*, sales_actual_leads.company_name, user_profiles.first_name as assigned_to_name')
|
||||
->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id', 'left')
|
||||
@ -139,6 +146,90 @@ class SalesActivityModel extends Model
|
||||
// ];
|
||||
}
|
||||
|
||||
public function getActivitiesWithFilters($filters = [], $limit = 10, $offset = 0)
|
||||
{
|
||||
// 1. Initial Selection
|
||||
$builder = $this->select("
|
||||
sales_activities.*,
|
||||
sales_actual_leads.company_name,
|
||||
up1.first_name as assigned_to_name,
|
||||
GROUP_CONCAT(DISTINCT up2.first_name SEPARATOR ', ') as additional_assigned_names
|
||||
")
|
||||
->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id', 'left')
|
||||
->join('user_profiles as up1', 'up1.id = sales_activities.assigned_to', 'left');
|
||||
|
||||
// 2. The JSON Join for additional names
|
||||
// Only attempts join if the string looks like a JSON array
|
||||
$builder->join('user_profiles as up2', "
|
||||
sales_activities.additional_assigned_ids IS NOT NULL
|
||||
AND sales_activities.additional_assigned_ids != ''
|
||||
AND sales_activities.additional_assigned_ids != '[]'
|
||||
AND JSON_VALID(sales_activities.additional_assigned_ids)
|
||||
AND JSON_CONTAINS(sales_activities.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR)))
|
||||
", 'left');
|
||||
|
||||
// 3. Apply Filters
|
||||
if (!empty($filters['status'])) {
|
||||
$builder->where('sales_activities.status', $filters['status']);
|
||||
}
|
||||
|
||||
if (!empty($filters['activity_type'])) {
|
||||
$builder->where('sales_activities.activity_type', $filters['activity_type']);
|
||||
}
|
||||
|
||||
if (!empty($filters['assigned_to'])) {
|
||||
$assignedToIds = is_array($filters['assigned_to']) ? $filters['assigned_to'] : explode(',', $filters['assigned_to']);
|
||||
$builder->whereIn('sales_activities.assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
if (!empty($filters['search'])) {
|
||||
$builder->groupStart()
|
||||
->like('sales_actual_leads.company_name', $filters['search'])
|
||||
->orLike('up1.first_name', $filters['search'])
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
// 4. Grouping & Ordering
|
||||
$builder->groupBy('sales_activities.activity_id');
|
||||
$builder->orderBy('sales_activities.created_at', 'DESC');
|
||||
|
||||
// 5. Calculate Counts (using a clean builder to avoid the syntax error)
|
||||
$counts = $this->getActivityStatusCounts($filters);
|
||||
|
||||
// 6. Get Data and Total
|
||||
// Use true for countAllResults to get an accurate count of grouped rows
|
||||
$totalCountQuery = clone $builder;
|
||||
$total = $totalCountQuery->countAllResults(false);
|
||||
|
||||
$data = $builder->findAll($limit, $offset);
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'total' => $total,
|
||||
'counts' => $counts
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get counts without breaking the main query syntax
|
||||
*/
|
||||
private function getActivityStatusCounts($filters)
|
||||
{
|
||||
$validStatuses = ['pending', 'completed'];
|
||||
$counts = ['all' => 0, 'pending' => 0, 'completed' => 0];
|
||||
|
||||
foreach ($validStatuses as $status) {
|
||||
$query = $this->db->table('sales_activities')->where('status', $status);
|
||||
if (!empty($filters['assigned_to'])) {
|
||||
$ids = is_array($filters['assigned_to']) ? $filters['assigned_to'] : explode(',', $filters['assigned_to']);
|
||||
$query->whereIn('assigned_to', $ids);
|
||||
}
|
||||
$counts[$status] = $query->countAllResults();
|
||||
}
|
||||
$counts['all'] = $counts['pending'] + $counts['completed'];
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete an activity
|
||||
*/
|
||||
|
||||
@ -123,7 +123,38 @@ class SalesActualLeadModel extends Model
|
||||
|
||||
$data = $this->findAll($limit, $offset);
|
||||
|
||||
return ['data' => $data,'total' => $total];
|
||||
$counts = $this->getLeadStatusCounts($filters);
|
||||
|
||||
return ['data' => $data,'total' => $total,'counts' => $counts];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get counts without breaking the main query syntax
|
||||
*/
|
||||
private function getLeadStatusCounts($filters)
|
||||
{
|
||||
|
||||
$validStatuses = ['New', 'Potential', 'Prospects', 'Not a Prospects'];
|
||||
$counts = ['all' => 0, 'New' => 0, 'Potential' => 0, 'Prospects' => 0, 'Not a Prospects' => 0];
|
||||
|
||||
foreach ($validStatuses as $status) {
|
||||
$countQuery = $this->db->table('sales_actual_leads')
|
||||
->whereIn('status', $validStatuses);
|
||||
|
||||
// Apply assigned_to filter to counts too
|
||||
if (!empty($filters['assigned_to'])) {
|
||||
$assignedToIds = is_array($filters['assigned_to'])
|
||||
? $filters['assigned_to']
|
||||
: explode(',', $filters['assigned_to']);
|
||||
$countQuery->whereIn('assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
$counts[$status] = $countQuery->where('status', $status)->countAllResults();
|
||||
}
|
||||
|
||||
$counts['all'] = array_sum($counts);
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -401,6 +401,12 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="form-row" id="lost_reason_div" style="display: none;">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="lost_reason">Lost Reason <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="lost_reason" name="lost_reason" rows="4" placeholder="Please specify why this opportunity was lost"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="notes">Remarks</label>
|
||||
@ -778,7 +784,8 @@
|
||||
$('#client_name').val(res.data.client_name);
|
||||
$('#client_short_name').val(res.data.client_short_name);
|
||||
$('#entity_type_id').val(res.data.entity_type_id);
|
||||
$('#lead_status').val(res.data.status);
|
||||
$('#lead_status').val(res.data.status).trigger('change');
|
||||
$('#lost_reason').val(res.data.lost_reason || '');
|
||||
$('#notes').val(res.data.notes);
|
||||
|
||||
setTimeout(function() {
|
||||
@ -2431,5 +2438,66 @@
|
||||
});
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
var actual_lead_client_details = <?= json_encode($actual_lead_client_details ?? null) ?>;
|
||||
var actual_lead_contact_person_details = <?= json_encode($actual_lead_contact_person_details ?? null) ?>;
|
||||
|
||||
// -------------------------------
|
||||
// CLIENT DETAILS AUTO FILL
|
||||
// -------------------------------
|
||||
if (actual_lead_client_details) {
|
||||
|
||||
$('#client_name').val(actual_lead_client_details.company_name || '');
|
||||
|
||||
$('#gst').val(actual_lead_client_details.gst_number || '');
|
||||
|
||||
// 🔥 Important:
|
||||
// Only auto-generate short name IF empty (avoid overwrite in edit)
|
||||
if (!$('#client_short_name').val()) {
|
||||
$('#client_name').trigger('input');
|
||||
} else {
|
||||
// Run duplicate validation once
|
||||
validateInput($('#client_short_name')[0], "clients", "short_name");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// CONTACT PERSON AUTO FILL
|
||||
// -------------------------------
|
||||
if (actual_lead_contact_person_details) {
|
||||
|
||||
$('#contact_person_name').val(actual_lead_contact_person_details.name || '');
|
||||
|
||||
$('#contact_person_mobile').val(actual_lead_contact_person_details.mobile || '');
|
||||
|
||||
$('#contact_person_email').val(actual_lead_contact_person_details.email || '');
|
||||
}
|
||||
|
||||
|
||||
$('#lead_status').change(function() {
|
||||
if ($(this).val() === 'lost') {
|
||||
$('#lost_reason_div').show();
|
||||
$('#lost_reason').attr('required', 'required');
|
||||
} else {
|
||||
$('#lost_reason_div').hide();
|
||||
$('#lost_reason').val("");
|
||||
$('#lost_reason').removeAttr('required');
|
||||
}
|
||||
});
|
||||
|
||||
// Check on page load
|
||||
if ($('#lead_status').val() === 'lost') {
|
||||
$('#lost_reason_div').show();
|
||||
$('#lost_reason').attr('required', 'required');
|
||||
} else {
|
||||
// Ensure it's hidden and not required if the initial value is not 'lost'
|
||||
$('#lost_reason_div').hide();
|
||||
$('#lost_reason').val("");
|
||||
$('#lost_reason').removeAttr('required');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------------------------------------
|
||||
</script>
|
||||
@ -410,6 +410,12 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-row" id="lost_reason_div" style="display: none;">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="lost_reason">Lost Reason <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="lost_reason" name="lost_reason" rows="4" placeholder="Please specify why this opportunity was lost"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="notes">Remarks</label>
|
||||
@ -1258,4 +1264,63 @@
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
var actual_lead_client_details = <?= json_encode($actual_lead_client_details ?? null) ?>;
|
||||
var actual_lead_contact_person_details = <?= json_encode($actual_lead_contact_person_details ?? null) ?>;
|
||||
|
||||
// -------------------------------
|
||||
// CLIENT DETAILS AUTO FILL
|
||||
// -------------------------------
|
||||
if (actual_lead_client_details) {
|
||||
|
||||
$('#client_name').val(actual_lead_client_details.company_name || '');
|
||||
|
||||
$('#gst').val(actual_lead_client_details.gst_number || '');
|
||||
|
||||
// 🔥 Important:
|
||||
// Only auto-generate short name IF empty (avoid overwrite in edit)
|
||||
if (!$('#client_short_name').val()) {
|
||||
$('#client_name').trigger('input');
|
||||
} else {
|
||||
// Run duplicate validation once
|
||||
validateInput($('#client_short_name')[0], "clients", "short_name");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// CONTACT PERSON AUTO FILL
|
||||
// -------------------------------
|
||||
if (actual_lead_contact_person_details) {
|
||||
|
||||
$('#contact_person_name').val(actual_lead_contact_person_details.name || '');
|
||||
|
||||
$('#contact_person_mobile').val(actual_lead_contact_person_details.mobile || '');
|
||||
|
||||
$('#contact_person_email').val(actual_lead_contact_person_details.email || '');
|
||||
}
|
||||
|
||||
|
||||
$('#lead_status').change(function() {
|
||||
if ($(this).val() === 'lost') {
|
||||
$('#lost_reason_div').show();
|
||||
$('#lost_reason').attr('required', 'required');
|
||||
} else {
|
||||
$('#lost_reason_div').hide();
|
||||
$('#lost_reason').val("");
|
||||
$('#lost_reason').removeAttr('required');
|
||||
}
|
||||
});
|
||||
|
||||
// Check on page load
|
||||
if ($('#lead_status').val() === 'lost') {
|
||||
$('#lost_reason_div').show();
|
||||
$('#lost_reason').attr('required', 'required');
|
||||
} else {
|
||||
// Ensure it's hidden and not required if the initial value is not 'lost'
|
||||
$('#lost_reason_div').hide();
|
||||
$('#lost_reason').val("");
|
||||
$('#lost_reason').removeAttr('required');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -10,6 +10,8 @@
|
||||
.btn-complete:hover { background: #4caf50; transform: translateY(-1px); }
|
||||
.btn-view { background: #f0f0f0; color: #666; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
|
||||
.btn-view:hover { background: #f0f0f0; transform: translateY(-1px); }
|
||||
.btn-close { background: none; border: none; font-size: 22px; cursor: pointer; color: #888; width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: background .2s; }
|
||||
.btn-close:hover { background: #f0f0f0; color: #333; }
|
||||
|
||||
/* Filter Tabs */
|
||||
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
||||
@ -68,6 +70,7 @@
|
||||
.activity-meta { display: flex; gap: 20px; font-size: 13px; color: #999; margin-top: 10px;}
|
||||
|
||||
.lead-status { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; margin-top: 5px; }
|
||||
.opportunity-status-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500;}
|
||||
|
||||
.status-new { background: #e3f2fd; color: #1976d2; }
|
||||
.status-potential { background: #fff3e0; color: #f57c00; }
|
||||
@ -76,6 +79,10 @@
|
||||
.status-pending { background: #fff3e0; color: #f57c00; }
|
||||
.status-completed { background: #e8f5e9; color: #388e3c; }
|
||||
.status-unknown { background: #000; color: #fff; }
|
||||
.status-text-unknown { color: #000; font-weight: bold; }
|
||||
.status-text-lost { color: #d32f2f; font-weight: bold; }
|
||||
.status-text-won { color: #388e3c; font-weight: bold; }
|
||||
|
||||
|
||||
/* Modals */
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center; }
|
||||
@ -124,6 +131,9 @@
|
||||
.opportunity-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;}
|
||||
.opportunity-detail-item { font-size: 13px; color: #666;}
|
||||
.opportunity-footer { margin-top: 15px; padding-top: 15px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #666; }
|
||||
.lost-reason { margin-bottom: 8px; color: #c0392b; /* soft red for lost */ }
|
||||
.footer-divider { border-top: 1px dashed #ddd; margin: 8px 0; }
|
||||
.notes { color: #555; }
|
||||
|
||||
/* Base style for both tabs */
|
||||
.tab-item { cursor: pointer; padding-bottom: 10px; margin: 0; font-size: 16px; color: #999; /* Default grey for unselected */ border-bottom: 2px solid transparent; transition: all 0.2s ease; }
|
||||
@ -142,6 +152,21 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.select2-container--default
|
||||
.select2-selection--multiple
|
||||
.select2-selection__choice {
|
||||
background-color: #02a8b5 !important;
|
||||
border: none !important;
|
||||
border-color: #fff !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #fff !important;
|
||||
}
|
||||
.modal .select2-container--default .select2-selection--multiple {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@ -159,7 +184,7 @@
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="lead-actions">
|
||||
<input type="text" class="search-input" id="mainSearch"
|
||||
placeholder="Search activities..." onkeyup="fetchActivities(false)">
|
||||
placeholder="Search activities..." onkeyup="fetchActivities(false)" style="width: 300px !important;">
|
||||
<button class="btn-primary" onclick="openMainActivityModal()">
|
||||
+ Add Activity
|
||||
</button>
|
||||
@ -191,7 +216,7 @@
|
||||
<h2 id="det_company">Lead Detail</h2>
|
||||
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
||||
</div>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('leadDetailModal')">×</button>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('leadDetailModal')">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
<div class="modal-body">
|
||||
@ -233,7 +258,7 @@
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Add Activity</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('activityModal')">×</button>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('activityModal')">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -276,11 +301,19 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||||
<select id="act_owner" class="search-input searchable" style="width:100%" required>
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Additional Assigner</label>
|
||||
<select id="act_multiple_owner" class="search-input searchable multi-searchable" style="width:100%;" multiple>
|
||||
<?php foreach($sales_team as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -297,7 +330,7 @@
|
||||
<div class="modal-content" style="max-width: 675px;">
|
||||
<div class="modal-header">
|
||||
<h3>Complete Activity</h3>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('completeModal')">×</button>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('completeModal')">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -346,11 +379,19 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||||
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Additional Assigner <span class="text-danger">*</span></label>
|
||||
<select id="f_multiple_owner" class="search-input searchable multi-searchable" style="width:100%" multiple>
|
||||
<?php foreach($sales_team as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -368,7 +409,7 @@
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Select Opportunity Type</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('opportunityModal')">×</button>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('opportunityModal')">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
<div class="modal-body p-4">
|
||||
@ -401,18 +442,45 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
resetFlatpicker();
|
||||
$('.searchable').each(function() {
|
||||
let parentModal = $(this).closest('.modal');
|
||||
$(this).select2({
|
||||
|
||||
// Single selects
|
||||
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Select..",
|
||||
dropdownParent: parentModal.length ? parentModal : $(document.body)
|
||||
allowClear: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body)
|
||||
});
|
||||
});
|
||||
|
||||
// Multi selects
|
||||
document.querySelectorAll('.multi-searchable').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Search and select members...",
|
||||
allowClear: false,
|
||||
closeOnSelect: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body),
|
||||
width: '100%'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// $(document).ready(function() {
|
||||
// resetFlatpicker();
|
||||
// $('.searchable').each(function() {
|
||||
// let parentModal = $(this).closest('.modal');
|
||||
// $(this).select2({
|
||||
// placeholder: "Select..",
|
||||
// dropdownParent: parentModal.length ? parentModal : $(document.body)
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
function resetFlatpicker(){
|
||||
document.querySelectorAll(".forschedule").forEach(el => {
|
||||
if (el._flatpickr) {
|
||||
@ -438,7 +506,7 @@ const activityIcons = {
|
||||
"Share Docs": "📄",
|
||||
"To Do": "✓"
|
||||
};
|
||||
const salesManagerIds = <?= json_encode($sales_manager_ids ?? []) ?>;
|
||||
const salesManagerWithHeadIds = <?= json_encode($sales_manager_with_head_ids ?? []) ?>;
|
||||
|
||||
const API = '<?= base_url('sales') ?>';
|
||||
let filter = 'all';
|
||||
@ -535,10 +603,10 @@ async function fetchActivities(isLoadMore = false) {
|
||||
</div>`;
|
||||
|
||||
console.log("function here");
|
||||
console.log(salesManagerIds);
|
||||
console.log(salesManagerWithHeadIds);
|
||||
// return;
|
||||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
||||
if (typeof salesManagerIds === 'undefined' || salesManagerIds.length === 0) {
|
||||
if (typeof salesManagerWithHeadIds === 'undefined' || salesManagerWithHeadIds.length === 0) {
|
||||
console.log("No Sales Manager IDs found. Skipping API call.");
|
||||
grid.innerHTML = emptyStateHTML;
|
||||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||||
@ -565,8 +633,8 @@ async function fetchActivities(isLoadMore = false) {
|
||||
// 5. Build URL with dynamic offset
|
||||
let url = `${API}/activities?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
|
||||
// url += `&assigned_to=${salesManagerIds.join(',')}`; // We already proved it exists above!
|
||||
if (typeof salesManagerIds !== 'undefined' && salesManagerIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerIds.join(',')}`;
|
||||
if (typeof salesManagerWithHeadIds !== 'undefined' && salesManagerWithHeadIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerWithHeadIds.join(',')}`;
|
||||
}
|
||||
|
||||
console.log("Fetching API:", url);
|
||||
@ -578,6 +646,15 @@ async function fetchActivities(isLoadMore = false) {
|
||||
const activityData = json.data || [];
|
||||
const totalrecords = json.total || 0;
|
||||
console.log("API Response:", activityData);
|
||||
|
||||
const tabLeadsCount = json.counts || {};
|
||||
|
||||
// ✅ Update tab counts
|
||||
if (tabLeadsCount) {
|
||||
document.querySelector('[data-filter="all"]').innerText = `All (${tabLeadsCount.all ?? 0})`;
|
||||
document.querySelector('[data-filter="Pending"]').innerText = `Pending (${tabLeadsCount.pending ?? 0})`;
|
||||
document.querySelector('[data-filter="Completed"]').innerText = `Completed (${tabLeadsCount.completed ?? 0})`;
|
||||
}
|
||||
|
||||
const html = activityData
|
||||
.sort((a, b) => Number(b.activity_id) - Number(a.activity_id))
|
||||
@ -613,9 +690,18 @@ async function fetchActivities(isLoadMore = false) {
|
||||
<div class="activity-lead-name">${a.company_name}</div>
|
||||
<div class="activity-notes" title="${displayNote}">${displayNote}</div>
|
||||
<div class="activity-meta">
|
||||
<span>📅 ${formattedCreatedDate}</span>
|
||||
<span>🗓️ ${formattedCreatedDate}</span>
|
||||
<span>👤 ${a.assigned_to_name}</span>
|
||||
</div>
|
||||
${a.additional_assigned_names ? `
|
||||
<div style="display:flex; justify-content:flex-start; margin-bottom:5px;">
|
||||
<span style="font-size:11px; color:#999; text-align: right;
|
||||
max-width: 75%; /* Forces long lists to wrap cleanly on the right side */
|
||||
display: inline-block;">
|
||||
👥 ${a.additional_assigned_names}
|
||||
</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="activity-actions">
|
||||
@ -706,10 +792,17 @@ async function viewDetail(id) {
|
||||
}
|
||||
|
||||
function renderCard(opps) {
|
||||
const cont = document.getElementById('opportunitiesContainer');
|
||||
|
||||
cont.innerHTML = opps.length ? opps.map(o => {
|
||||
const opp_cont = document.getElementById('opportunitiesContainer');
|
||||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
|
||||
let status = o.status?.toLowerCase();
|
||||
let statusClass = {
|
||||
won: 'status-text-won',
|
||||
lost: 'status-text-lost'
|
||||
}[status] || 'status-text-unknown';
|
||||
|
||||
let opp_status_value = o.status?.replace(/[-_']/g, ' ').toUpperCase();
|
||||
|
||||
let formattedDate = new Date(o.created_at).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
@ -736,11 +829,21 @@ function renderCard(opps) {
|
||||
<strong>Created At:</strong> ${formattedDate}
|
||||
</div>
|
||||
<div class="opportunity-detail-item">
|
||||
<strong>Status:</strong> ${o.status}
|
||||
<strong>Status:</strong> <span class="${statusClass}">${opp_status_value}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opportunity-footer">
|
||||
${o.notes || 'N/A'}
|
||||
${ `${o.status === 'lost' ? `
|
||||
<div class="lost-reason">
|
||||
<strong>Reason for Loss:</strong> ${o.lost_reason || 'N/A'}
|
||||
</div>
|
||||
<div class="footer-divider"></div>
|
||||
` : ``}
|
||||
<div class="notes">
|
||||
<strong>Remarks:</strong> ${o.notes || 'N/A'}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@ -774,24 +877,35 @@ function renderTimeline(acts) {
|
||||
// <span style="font-size:11px; color:#999">${formattedDate}</span>
|
||||
|
||||
return `
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
|
||||
<div class="timeline-content">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
|
||||
<b>${icon} ${formattedType}</b>
|
||||
<span style="font-size:11px; color:#999"> 👤 ${a.assigned_to_name} | 🗓 ${formattedDate}</span>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:2px;">
|
||||
<b style="white-space: nowrap; margin-right: 15px;">${icon} ${formattedType}</b>
|
||||
<span style="font-size:11px; color:#999; text-align:right;"> 👤 ${a.assigned_to_name} | 🗓️ ${formattedDate}</span>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; font-size:13px; color:#444;">
|
||||
|
||||
${a.additional_assigned_names ? `
|
||||
<div style="display:flex; justify-content:flex-end; margin-bottom:5px;">
|
||||
<span style="font-size:11px; color:#999; text-align: right;
|
||||
max-width: 75%; /* Forces long lists to wrap cleanly on the right side */
|
||||
display: inline-block;">
|
||||
👥 ${a.additional_assigned_names}
|
||||
</span>
|
||||
</div>
|
||||
` : '<div style="margin-bottom:8px;"></div>'}
|
||||
|
||||
<div style="font-size:13px; color:#444; margin-bottom: 8px;">
|
||||
${a.notes}
|
||||
</div>
|
||||
<div style="font-size:13px; color:#444;"></div>
|
||||
|
||||
${a.status === 'pending' ?
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:8px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id},'frompopup')">Mark Complete</button>` :
|
||||
`<div style="font-size:12px; color:#388e3c; margin-top:8px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:4px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
|
||||
`<div style="font-size:12px; color:#388e3c; margin-top:4px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</div>`;
|
||||
}
|
||||
|
||||
@ -906,7 +1020,8 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
notes: actNotes, // Reused the trimmed variable from above
|
||||
scheduled_date: dbFormat,
|
||||
assigned_to: actOwner, // Reused the trimmed variable from above
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
additional_assigned_ids : Array.from(document.getElementById('act_multiple_owner').selectedOptions).map(o => o.value)
|
||||
};
|
||||
|
||||
const res = await fetch(`${API}/activities`, {
|
||||
@ -1023,7 +1138,8 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
notes: document.getElementById('f_notes').value,
|
||||
scheduled_date: dbFormat,
|
||||
assigned_to: document.getElementById('f_assigned_to').value,
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
additional_assigned_ids : Array.from(document.getElementById('f_multiple_owner').selectedOptions).map(o => o.value)
|
||||
};
|
||||
|
||||
const res2 = await fetch(`${API}/activities`, {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
294
app/Views/sales/branch_level_dashboard_view_1mar.php
Normal file
294
app/Views/sales/branch_level_dashboard_view_1mar.php
Normal file
@ -0,0 +1,294 @@
|
||||
<style>
|
||||
.dash-container { padding: 25px; background: #f8f9fa; font-family: 'Segoe UI', sans-serif; }
|
||||
.top-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; }
|
||||
|
||||
.stat-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; }
|
||||
.card { background: white; padding: 25px; border-radius: 12px; border: 1px solid #edf2f7; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
|
||||
.stat-val { font-size: 28px; font-weight: 700; color: #1a202c; }
|
||||
.stat-label { color: #718096; font-size: 14px; margin-top: 4px; font-weight: 500; }
|
||||
.stat-change { font-size: 11px; margin-top: 8px; font-weight: 600; }
|
||||
.text-success { color: #48bb78; }
|
||||
|
||||
.card-hero {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #edf2f7;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* The Gradient Overlay Effect */
|
||||
.card-hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px; /* Thin line at the top */
|
||||
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.card-hero:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Optional: Subtle Background Gradient */
|
||||
.card-hero.gradient-bg {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
|
||||
}
|
||||
|
||||
.main-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 30px; }
|
||||
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
|
||||
.team-member { display: flex; align-items: center; padding: 15px 0; border-bottom: 1px solid #f1f5f9; }
|
||||
.member-img { width: 40px; height: 40px; border-radius: 50%; background: #ff6b35; color: white; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 16px; }
|
||||
.act-stats { text-align: right; }
|
||||
.act-stats .total { font-weight: 700; font-size: 13px; color: #1a202c; }
|
||||
.act-stats .done { font-size: 11px; color: #38a169; font-weight: 600; margin-top: 2px; }
|
||||
|
||||
.breakdown-card { margin-top: 20px; }
|
||||
.breakdown-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 13px; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 8px; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { text-align: left; padding: 12px; color: #718096; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid #edf2f7; }
|
||||
td { padding: 15px 12px; border-bottom: 1px solid #f1f5f9; font-size: 14px; vertical-align: middle; }
|
||||
|
||||
.status-pill { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; }
|
||||
.status-completed { background: #f0fff4; color: #38a169; }
|
||||
.status-pending { background: #fffaf0; color: #dd6b20; }
|
||||
.status-new { background: #e3f2fd; color: #1976d2; }
|
||||
.status-potential { background: #fff3e0; color: #f57c00; }
|
||||
.status-prospects { background: #e8f5e9; color: #388e3c; }
|
||||
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
||||
|
||||
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
|
||||
.empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px; }
|
||||
|
||||
</style>
|
||||
|
||||
<div class="dash-container">
|
||||
<!-- <div class="top-header">
|
||||
<div>
|
||||
<h2 style="font-weight: 800; color: #1a202c; font-size: 24px;">Dashboard</h2>
|
||||
<p style="color: #718096; font-size: 14px; margin-top: 4px;">Overview of your branch</p>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<input type="text" placeholder="Search leads, activities....." style="background:white; padding:12px 20px; border-radius:10px; border:1px solid #e2e8f0; width: 350px; font-size: 13px;">
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="stat-cards">
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo $total_leads; ?></div>
|
||||
<div class="stat-label">Total Leads</div>
|
||||
<!-- <div class="stat-change text-success">↑ 12% this month</div> -->
|
||||
</div>
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo $total_acts ?></div>
|
||||
<div class="stat-label">Total Activities</div>
|
||||
<!-- <div class="stat-change text-success">↑ 5% this month</div> -->
|
||||
</div>
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo $total_pending_acts ?></div>
|
||||
<div class="stat-label">Pending Activities</div>
|
||||
<!-- <div class="stat-change text-success">↑ 15% this month</div> -->
|
||||
</div>
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo $total_completed_acts ?></div>
|
||||
<div class="stat-label">Completed Activities</div>
|
||||
<!-- <div class="stat-change text-success">↑ 15% this month</div> -->
|
||||
</div>
|
||||
<!-- <div class="card">
|
||||
<div class="stat-val">₹15.0L</div>
|
||||
<div class="stat-label">Pipeline Value</div>
|
||||
<div class="stat-change text-success">↑ 18% this month</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="main-grid">
|
||||
<div class="card">
|
||||
<div class="table-header">
|
||||
<h3 style="font-size: 16px; font-weight: 700;">Pending Activities - All Team</h3>
|
||||
<span style="color: #718096; font-size: 12px; font-weight: 600;"><?= count($pending_acts) . ' ' . (count($pending_acts) == 1 ? 'activity' : 'activities') ?></span>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Company</th>
|
||||
<th>Activity & Assigned</th>
|
||||
<th>Date</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($pending_acts)): ?>
|
||||
<?php foreach ($pending_acts as $a): ?>
|
||||
<?php
|
||||
$activityIcons = [
|
||||
'Call' => '📞',
|
||||
'Email' => '✉️',
|
||||
'Meeting' => '📅',
|
||||
'Visit' => '🚗',
|
||||
'Demo' => '🖥️',
|
||||
'Share Docs' => '📄',
|
||||
'To Do' => '✓'
|
||||
];
|
||||
$icon = $activityIcons[$a['activity_type']] ?? '📌';
|
||||
$statusClass = strtolower(str_replace(' ', '-', $a['status']));
|
||||
$formattedscheduledDate = date('M d, Y, h:i A', strtotime($a['scheduled_date']));
|
||||
?>
|
||||
<tr>
|
||||
<td><strong><?= esc($a['company_name']) ?></strong></td>
|
||||
<td>
|
||||
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">
|
||||
<?= $icon ?> <?= strtoupper(esc($a['activity_type'])) ?>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #718096;">
|
||||
<?= esc($a['assigned_to_name'] ?? 'ID: ' . $a['assigned_to']) ?>
|
||||
</div>
|
||||
<!-- <div style="font-size: 12px; color: #718096;">
|
||||
<?= esc($a['notes'] ?? 'ID: ' . $a['assigned_to']) ?>
|
||||
</div> -->
|
||||
</td>
|
||||
<td style="color: #4a5568; font-weight: 500;"><?= $formattedscheduledDate ?></td>
|
||||
<td><span class="status-pill status-<?= $statusClass ?>"><?= ucfirst(esc($a['status'])) ?></span></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">✓</div> No activities found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
|
||||
<h3 style="font-size: 16px; font-weight: 700; margin-bottom: 15px;">Sales Team Performance</h3>
|
||||
<?php foreach ($team as $member) {
|
||||
$firstLetter = strtoupper(substr($member['first_name'], 0, 1));
|
||||
$fullName = $member['first_name'] . ' ' . $member['last_name'];
|
||||
$totalActs = $member['total_acts'];
|
||||
$doneActs = $member['done_acts'];
|
||||
$role = $member['role']; // fix your DB first
|
||||
|
||||
// Random or consistent color based on name
|
||||
$colors = ['#ff6b35', '#667eea', '#48bb78', '#ed8936', '#9f7aea'];
|
||||
$colorIndex = abs(crc32($member['first_name'])) % count($colors);
|
||||
$color = $colors[$colorIndex];
|
||||
|
||||
echo "
|
||||
<div class='team-member'>
|
||||
<div class='member-img' style='background: {$color};'>{$firstLetter}</div>
|
||||
<div style='flex: 1;'>
|
||||
<div style='font-weight: 700; font-size: 14px;'>{$fullName}</div>
|
||||
<div style='font-size: 11px; color: #718096;'>{$role}</div>
|
||||
</div>
|
||||
<div class='act-stats'>
|
||||
<div class='total'>{$totalActs} acts</div>
|
||||
<div class='done'>{$doneActs} done</div>
|
||||
</div>
|
||||
</div>";
|
||||
} ?>
|
||||
|
||||
|
||||
<div class="card breakdown-card">
|
||||
<h3 style="font-size: 15px; font-weight: 700; margin-bottom: 20px;">Activity Breakdown</h3>
|
||||
<!-- <div class="breakdown-item"><span><span class="dot" style="background: #ff6b35;"></span> Call</span><strong>42%</strong></div>
|
||||
<div class="breakdown-item"><span><span class="dot" style="background: #48bb78;"></span> Email</span><strong>25%</strong></div>
|
||||
<div class="breakdown-item"><span><span class="dot" style="background: #4299e1;"></span> Meeting</span><strong>18%</strong></div>
|
||||
<div class="breakdown-item"><span><span class="dot" style="background: #ecc94b;"></span> Visit</span><strong>10%</strong></div>
|
||||
<div class="breakdown-item"><span><span class="dot" style="background: #9f7aea;"></span> Demo</span><strong>5%</strong></div> -->
|
||||
<?php
|
||||
$activityConfig = [
|
||||
'Call' => ['color' => '#ff6b35', 'icon' => '📞'],
|
||||
'Email' => ['color' => '#48bb78', 'icon' => '✉️'],
|
||||
'Meeting' => ['color' => '#4299e1', 'icon' => '📅'],
|
||||
'Visit' => ['color' => '#ecc94b', 'icon' => '🚗'],
|
||||
'Demo' => ['color' => '#9f7aea', 'icon' => '🖥️'],
|
||||
'Share Docs' => ['color' => '#ed8936', 'icon' => '📄'],
|
||||
'To Do' => ['color' => '#718096', 'icon' => '✓'],
|
||||
];
|
||||
?>
|
||||
|
||||
<?php if (!empty($activity_breakdown)): ?>
|
||||
<?php foreach ($activity_breakdown as $item): ?>
|
||||
<?php
|
||||
$type = $item['activity_type'];
|
||||
$color = $activityConfig[$type]['color'] ?? '#718096';
|
||||
$icon = $activityConfig[$type]['icon'] ?? '📌';
|
||||
?>
|
||||
<div class="breakdown-item">
|
||||
<span>
|
||||
<span style="background: <?= $color ?>;"></span>
|
||||
<?= $icon ?> <?= esc($type) ?>
|
||||
</span>
|
||||
<strong><?= $item['percentage'] ?>%</strong>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">✓</div> No activities found
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="grid-column: 1 / -1; width: 100%; box-sizing: border-box;">
|
||||
<div class="table-header">
|
||||
<h3 style="font-size: 16px; font-weight: 700;">All Leads Overview </h3>
|
||||
<!-- <span style="color: #718096; font-size: 12px; font-weight: 600;">Click a lead to see details</span>
|
||||
<a href="<?= base_url('sales') ?>"
|
||||
style="color: #718096; font-size: 12px; font-weight: 600; text-decoration: none; padding: 4px 8px; transition: color 0.2s ease; display: inline-block; cursor: pointer;"
|
||||
onmouseover="this.style.color='#ff6b35';"
|
||||
onmouseout="this.style.color='#718096';">
|
||||
Click a lead to see details
|
||||
</a> -->
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Company</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned To</th>
|
||||
<th style="text-align: center !important;">Activities</th>
|
||||
<th style="text-align: center !important;">Opportunities</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($leads_overview)): ?>
|
||||
<?php foreach ($leads_overview as $lead): ?>
|
||||
<?php $statusClass = strtolower(str_replace(' ', '-', $lead['status'])); ?>
|
||||
<tr>
|
||||
<td><strong><?= esc($lead['company_name']) ?></strong></td>
|
||||
<td><span class="status-pill status-<?= $statusClass ?>"><?= esc($lead['status']) ?></span></td>
|
||||
<td><?= esc($lead['assigned_to'] ?? 'Unassigned') ?></td>
|
||||
<td><div style="text-align: center; font-weight: 700;"><?= $lead['activities'] ?></div></td>
|
||||
<td><div style="text-align: center; font-weight: 700;"><?= $lead['opportunities'] ?></div></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">👤</div> No leads found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -74,7 +74,7 @@
|
||||
<p style="color:#666; font-size:13px; margin-top: 4px;">Welcome back, <?= $user_name ?></p> -->
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<select id="financial_year" name="financial_year" onchange="onFinancialYearChange(this)" style="background:#f5f5f5; padding:10px 20px; border-radius:8px; border:none; width: 250px; font-size: 13px; cursor: pointer;">
|
||||
<select id="financial_year" name="financial_year" onchange="onFinancialYearChange(this)" style="width: 250px; font-size: 13px; cursor: pointer;">
|
||||
<?php if(!empty($fin_years)): ?>
|
||||
<?php foreach($fin_years as $year): ?>
|
||||
<option value="<?= $year; ?>"><?= $year; ?></option>
|
||||
@ -133,6 +133,9 @@
|
||||
<button class="badge-done" onclick="openComp(<?= $u['activity_id'] ?>,<?= $u['lead_id'] ?>)">✓ Mark as completed</button>
|
||||
</div>
|
||||
<?php endforeach; ?> -->
|
||||
<?php if (!empty($upcoming)) : ?>
|
||||
|
||||
|
||||
<?php foreach($upcoming as $u):
|
||||
$activityIcons = [ 'Call' => '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share Docs' => '📄', 'To Do' => '✓' ];
|
||||
$icon = $activityIcons[$u['activity_type']] ?? '📌';
|
||||
@ -160,11 +163,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
|
||||
<div class="text-center text-muted py-3">
|
||||
No Upcoming Activities for this Financial Year.
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="list-card">
|
||||
<h3 style="font-size:16px; font-weight: 700; margin-bottom:20px;">My Recent Leads</h3>
|
||||
<?php if (!empty($recent_leads)) : ?>
|
||||
<?php foreach($recent_leads as $rl): ?>
|
||||
<!-- <div class="list-item">
|
||||
<div style="display:flex; gap:12px; align-items:center;">
|
||||
@ -206,6 +217,13 @@
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
|
||||
<div class="text-center text-muted py-3">
|
||||
No Leads Found for this Financial Year.
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -307,12 +307,12 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const users = <?= json_encode($users ?? []) ?>;
|
||||
const sales_manager = <?= json_encode($sales_manager ?? []) ?>;
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
DATA
|
||||
────────────────────────────────────────── */
|
||||
const teamMembers = users.map(user => ({
|
||||
const teamMembers = sales_manager.map(user => ({
|
||||
id: user.id,
|
||||
name: user.first_name + ' ' + user.last_name
|
||||
}));
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
|
||||
.btn-primary { background: #02a8b5; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
|
||||
.btn-primary:hover { background: #02a8b5; transform: translateY(-1px); }
|
||||
.btn-close { background: none; border: none; font-size: 22px; cursor: pointer; color: #888; width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: background .2s; }
|
||||
.btn-close:hover { background: #f0f0f0; color: #333; }
|
||||
|
||||
/* Filter Tabs */
|
||||
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
||||
@ -24,6 +26,11 @@
|
||||
.status-potential { background: #fff3e0; color: #f57c00; }
|
||||
.status-prospects { background: #e8f5e9; color: #388e3c; }
|
||||
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
||||
.status-unknown { background: #000; color: #fff; }
|
||||
.status-text-unknown { color: #000; font-weight: bold; }
|
||||
.status-text-lost { color: #d32f2f; font-weight: bold; }
|
||||
.status-text-won { color: #388e3c; font-weight: bold; }
|
||||
|
||||
|
||||
/* Modals */
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center; }
|
||||
@ -72,6 +79,9 @@
|
||||
.opportunity-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;}
|
||||
.opportunity-detail-item { font-size: 13px; color: #666;}
|
||||
.opportunity-footer { margin-top: 15px; padding-top: 15px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #666; }
|
||||
.lost-reason { margin-bottom: 8px; color: #c0392b; /* soft red for lost */ }
|
||||
.footer-divider { border-top: 1px dashed #ddd; margin: 8px 0; }
|
||||
.notes { color: #555; }
|
||||
|
||||
/* Base style for both tabs */
|
||||
.tab-item { cursor: pointer; padding-bottom: 10px; margin: 0; font-size: 16px; color: #999; /* Default grey for unselected */ border-bottom: 2px solid transparent; transition: all 0.2s ease; }
|
||||
@ -105,7 +115,20 @@
|
||||
border-color: #ff4d4d !important;
|
||||
}
|
||||
|
||||
|
||||
.select2-container--default
|
||||
.select2-selection--multiple
|
||||
.select2-selection__choice {
|
||||
background-color: #02a8b5 !important;
|
||||
border: none !important;
|
||||
border-color: #fff !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #fff !important;
|
||||
}
|
||||
.modal .select2-container--default .select2-selection--multiple {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="main-content">
|
||||
@ -137,7 +160,7 @@
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="lead-actions">
|
||||
<input type="text" class="search-input" id="mainSearch"
|
||||
placeholder="Search leads..." onkeyup="fetchLeads(false)">
|
||||
placeholder="Search leads..." onkeyup="fetchLeads(false)" style="width: 300px !important;">
|
||||
<button class="btn-primary" onclick="openModal('addLeadModal')">
|
||||
+ Add Lead
|
||||
</button>
|
||||
@ -166,7 +189,7 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add Lead</h4>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('addLeadModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('addLeadModal')" title="Close">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="addLeadForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -203,8 +226,8 @@
|
||||
<label class="form-label">Assign To<span class="text-danger">*</span></label>
|
||||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||||
<option value="">Select User</option>
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@ -228,7 +251,7 @@
|
||||
<h2 id="det_company">Lead Detail</h2>
|
||||
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
||||
</div>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('leadDetailModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('leadDetailModal')" title="Close">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
<div class="modal-body">
|
||||
@ -270,7 +293,7 @@
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Add Activity</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('activityModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('activityModal')" title="Close">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -305,12 +328,20 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assigned To</label>
|
||||
<select id="act_owner" class="search-input searchable" style="width:100%">
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Additional Assigner</label>
|
||||
<select id="act_multiple_owner" class="search-input searchable multi-searchable" style="width:100%;" multiple>
|
||||
<?php foreach($sales_team as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group text-right mb-0">
|
||||
@ -326,7 +357,7 @@
|
||||
<div class="modal-content" style="max-width: 675px;">
|
||||
<div class="modal-header">
|
||||
<h3>Complete Activity</h3>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('completeModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('completeModal')" title="Close">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -374,12 +405,20 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||||
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Additional Assigner <span class="text-danger">*</span></label>
|
||||
<select id="f_multiple_owner" class="search-input searchable multi-searchable" style="width:100%" multiple>
|
||||
<?php foreach($sales_team as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -397,7 +436,7 @@
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Select Opportunity Type</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('opportunityModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('opportunityModal')" title="Close">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
<div class="modal-body p-4">
|
||||
@ -429,61 +468,12 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="modal" id="opportunityModal">
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Add Opportunity</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('opportunityModal')">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="OpportunityForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
<div class="modal-body p-4">
|
||||
|
||||
<input type="hidden" id="opp_lead_id">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Opportunity Title</label>
|
||||
<input type="text" name="title" class="search-input" style="width:100%">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group ml-1">
|
||||
<label class="form-label">Expected Worth Amount</label>
|
||||
<input type="number" name="amount" class="search-input" style="width:100%">
|
||||
</div>
|
||||
<div class="form-group mr-1">
|
||||
<label class="form-label">Policy Type</label>
|
||||
<input type="text" name="policy_type" class="search-input" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Employee Count</label>
|
||||
<input type="number" name="count" class="search-input" style="width:100%">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Expected Close Date</label>
|
||||
<input type="datetime-local" id="e_date" class="search-input" style="width:100%">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea id="opp_notes" class="search-input" style="width:100%; height:100px;" placeholder="Add notes about this opportunity..." required></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group text-right mb-0">
|
||||
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('opportunityModal')">Cancel</button>
|
||||
<button type="submit" class="btn-primary" >Create Opportunity</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="modal" id="editLeadModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Edit Lead</h4>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('editLeadModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('editLeadModal')" title="Close">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="editLeadForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -525,46 +515,75 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-1" style="padding-left: 25px;">
|
||||
<label class="form-label">Contact Persons</label>
|
||||
<div id="contactPersonsContainer">
|
||||
<div id="savedContactsContainer"> <span id="NoData"> <center><i> No contact persons added yet </i> </center> </span> </div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.g-0-5 { --bs-gutter-x: 0.3rem; }
|
||||
.g-0-5 > * { padding-right: calc(var(--bs-gutter-x) * .5); padding-left: calc(var(--bs-gutter-x) * .5); }
|
||||
|
||||
<div class="col-12 mb-1 ml-1 ">
|
||||
|
||||
<div class="row g-2 align-items-center">
|
||||
|
||||
<div class="col-md-5" style="padding-left: 20px;">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="contact_name"
|
||||
placeholder="Contact Person Name"
|
||||
oninput="this.value=this.value.replace(/[^A-Za-z\s]/g,'')">
|
||||
.primary-container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Ensures the checkbox container height matches the standard input height */
|
||||
height: 31px;
|
||||
}
|
||||
.primary-label {
|
||||
font-size: 12px !important;
|
||||
font-weight: bold;
|
||||
/* Forces the text to take up exactly its own height */
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
color: #888;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* This is the most important part */
|
||||
.primary-container .form-check-input {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
cursor: pointer;
|
||||
/* Reset Bootstrap's default position: absolute if present */
|
||||
position: static;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="col-12 ml-1">
|
||||
<label class="form-label fw-bold small ml-1">Contact Persons</label>
|
||||
<input type="hidden" id="editing_contact_id" value="">
|
||||
<div class="row g-0-5 align-items-end ml-1">
|
||||
<div class="col-md-3 col-12 mb-2 mb-md-0">
|
||||
<input type="text" class="form-control form-control-sm" id="contact_name" placeholder="Person Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="contact_mobile"
|
||||
placeholder="Contact Mobile Number"
|
||||
maxlength="10"
|
||||
oninput="this.value=this.value.replace(/[^0-9]/g,'')">
|
||||
<div class="col-md-3 col-6">
|
||||
<input type="text" class="form-control form-control-sm" id="contact_mobile" placeholder="Mobile" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10);">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" style="padding-right: 4%;">
|
||||
<button type="button"
|
||||
id="btnSaveContact"
|
||||
class="btn btn-sm w-100"
|
||||
style="background:#ff6a3d;border:none;color:white;">
|
||||
+ Save Contact
|
||||
<div class="col-md-3 col-6">
|
||||
<input type="text" class="form-control form-control-sm" id="contact_designation" placeholder="Designation" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')">
|
||||
</div>
|
||||
|
||||
<div class="col-md-1 col-3">
|
||||
<div class="primary-container">
|
||||
<label for="contact_is_primary" class="primary-label">Primary</label>
|
||||
<input class="form-check-input" type="checkbox" id="contact_is_primary">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-6">
|
||||
<button type="button" id="btnSaveContact" class="btn btn-sm btn-info text-white" style="width:85%">
|
||||
✔ Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="contactPersonsList" class="p-1">
|
||||
<div id="noContactPersonsData" class="py-1 text-center">
|
||||
<small class="text-muted fst-italic">No contact persons added yet</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mb-1">
|
||||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||||
@ -580,8 +599,8 @@
|
||||
<label class="form-label">Assign To <span class="text-danger">*</span> </label>
|
||||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||||
<option value="">Select User</option>
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@ -599,16 +618,32 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
resetFlatpicker();
|
||||
$('.searchable').each(function() {
|
||||
let parentModal = $(this).closest('.modal');
|
||||
$(this).select2({
|
||||
|
||||
// Single selects
|
||||
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Select..",
|
||||
dropdownParent: parentModal.length ? parentModal : $(document.body)
|
||||
allowClear: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body)
|
||||
});
|
||||
});
|
||||
|
||||
// Multi selects
|
||||
document.querySelectorAll('.multi-searchable').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Search and select members...",
|
||||
allowClear: false,
|
||||
closeOnSelect: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body),
|
||||
width: '100%'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function resetFlatpicker(){
|
||||
@ -626,7 +661,7 @@ function resetFlatpicker(){
|
||||
allowInput: false,
|
||||
});
|
||||
}
|
||||
const salesManagerIds = <?= json_encode($sales_manager_ids ?? []) ?>;
|
||||
const salesManagerHeadIds = <?= json_encode($sales_manager_with_head_ids ?? []) ?>;
|
||||
const API = '<?= base_url('sales') ?>';
|
||||
let filter = 'all';
|
||||
let lead_id = null;
|
||||
@ -648,7 +683,12 @@ function closeModal(id) {
|
||||
const form = modal.querySelector('form'); // Find the form inside this specific modal
|
||||
if (form) {
|
||||
form.reset(); // Resets standard inputs (text, email, select)
|
||||
$(form).find('.searchable').val('').trigger('change'); // Clear jQuery/Select2/Searchable dropdowns if you use them
|
||||
form.querySelectorAll('.searchable').forEach(function(el) {
|
||||
// Reset native select
|
||||
Array.from(el.options).forEach(o => o.selected = false);
|
||||
// Notify Select2 to refresh its UI
|
||||
$(el).trigger('change');
|
||||
});
|
||||
$(form).find('.is-invalid').removeClass('is-invalid'); // Remove any "is-invalid" red borders from previous validation errors
|
||||
}
|
||||
|
||||
@ -672,6 +712,15 @@ function closeModal(id) {
|
||||
switchTab('activity'); // Reset the tab back to 'activity'
|
||||
document.getElementById('btn_add_opportunity').style.display = 'none';
|
||||
}
|
||||
|
||||
if (id === 'editLeadModal') {
|
||||
// const Eform = document.getElementById('editLeadForm');
|
||||
// if (!Eform) return; // safety check
|
||||
// Eform.reset();
|
||||
form.querySelectorAll('[name="status"] option')
|
||||
.forEach(opt => opt.hidden = false);
|
||||
form.querySelector('[name="status"]').value = 'New';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -733,7 +782,7 @@ async function fetchLeads(isLoadMore = false) {
|
||||
</div>`;
|
||||
|
||||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
||||
if (typeof salesManagerIds === 'undefined' || salesManagerIds.length === 0) {
|
||||
if (typeof salesManagerHeadIds === 'undefined' || salesManagerHeadIds.length === 0) {
|
||||
console.log("No Sales Manager IDs found. Skipping API call.");
|
||||
grid.innerHTML = emptyStateHTML;
|
||||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||||
@ -759,9 +808,9 @@ async function fetchLeads(isLoadMore = false) {
|
||||
|
||||
// 5. Build URL with dynamic offset
|
||||
let url = `${API}/leads?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
|
||||
// url += `&assigned_to=${salesManagerIds.join(',')}`; // We already proved it exists above!
|
||||
if (typeof salesManagerIds !== 'undefined' && salesManagerIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerIds.join(',')}`;
|
||||
// url += `&assigned_to=${salesManagerHeadIds.join(',')}`; // We already proved it exists above!
|
||||
if (typeof salesManagerHeadIds !== 'undefined' && salesManagerHeadIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerHeadIds.join(',')}`;
|
||||
}
|
||||
|
||||
console.log("Fetching API:", url);
|
||||
@ -773,6 +822,17 @@ async function fetchLeads(isLoadMore = false) {
|
||||
const leadsData = json.data || [];
|
||||
const totalrecords = json.total || 0;
|
||||
console.log("API Response:", leadsData);
|
||||
|
||||
const tabLeadsCount = json.counts || {};
|
||||
|
||||
// ✅ Update tab counts
|
||||
if (tabLeadsCount) {
|
||||
document.querySelector('[data-filter="all"]').innerText = `All (${tabLeadsCount.all ?? 0})`;
|
||||
document.querySelector('[data-filter="New"]').innerText = `New (${tabLeadsCount.New ?? 0})`;
|
||||
document.querySelector('[data-filter="Potential"]').innerText = `Potential (${tabLeadsCount.Potential ?? 0})`;
|
||||
document.querySelector('[data-filter="Prospects"]').innerText = `Prospects (${tabLeadsCount.Prospects ?? 0})`;
|
||||
document.querySelector('[data-filter="Not a Prospects"]').innerText = `Not a Prospects (${tabLeadsCount['Not a Prospects'] ?? 0})`;
|
||||
}
|
||||
|
||||
const html = leadsData
|
||||
.sort((a, b) => Number(b.lead_id) - Number(a.lead_id))
|
||||
@ -801,7 +861,7 @@ async function fetchLeads(isLoadMore = false) {
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<a class="text-body dropdown-toggle" href="#" data-toggle="dropdown">
|
||||
<i class="mdi mdi-dots-vertical font-20" title="Options"></i>
|
||||
<i class="mdi mdi-dots-vertical font-20 btn-close" title="Options"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="javascript:void(0);" onclick="viewDetail(${l.lead_id})"> 👁 View</a>
|
||||
@ -894,10 +954,21 @@ async function viewDetail(id) {
|
||||
}
|
||||
|
||||
function renderCard(opps) {
|
||||
const cont = document.getElementById('opportunitiesContainer');
|
||||
const opp_cont = document.getElementById('opportunitiesContainer');
|
||||
|
||||
cont.innerHTML = opps.length ? opps.map(o => {
|
||||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
|
||||
let status = o.status?.toLowerCase();
|
||||
let statusClass = {
|
||||
won: 'status-text-won',
|
||||
lost: 'status-text-lost'
|
||||
}[status] || 'status-text-unknown';
|
||||
|
||||
let opp_status_value = o.status?.replace(/[-_']/g, ' ').toUpperCase();
|
||||
|
||||
|
||||
|
||||
|
||||
let formattedDate = new Date(o.created_at).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
@ -924,11 +995,21 @@ function renderCard(opps) {
|
||||
<strong>Created At:</strong> ${formattedDate}
|
||||
</div>
|
||||
<div class="opportunity-detail-item">
|
||||
<strong>Status:</strong> ${o.status}
|
||||
<strong>Status:</strong> <span class="${statusClass}">${opp_status_value}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opportunity-footer">
|
||||
${o.notes || 'N/A'}
|
||||
${ `${o.status === 'lost' ? `
|
||||
<div class="lost-reason">
|
||||
<strong>Reason for Loss:</strong> ${o.lost_reason || 'N/A'}
|
||||
</div>
|
||||
<div class="footer-divider"></div>
|
||||
` : ``}
|
||||
<div class="notes">
|
||||
<strong>Remarks:</strong> ${o.notes || 'N/A'}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@ -936,15 +1017,7 @@ function renderCard(opps) {
|
||||
}
|
||||
function renderTimeline(acts) {
|
||||
const cont = document.getElementById('timelineContainer');
|
||||
const activityIcons = {
|
||||
"Call": "📞",
|
||||
"Email": "✉️",
|
||||
"Meeting": "📅",
|
||||
"Visit": "🚗",
|
||||
"Demo": "🖥️",
|
||||
"Share Docs": "📄",
|
||||
"To Do": "✓"
|
||||
};
|
||||
const activityIcons = { "Call": "📞", "Email": "✉️", "Meeting": "📅", "Visit": "🚗", "Demo": "🖥️", "Share Docs": "📄", "To Do": "✓"};
|
||||
|
||||
if (acts.length === 0) {
|
||||
cont.classList.add('no-line');
|
||||
@ -969,29 +1042,41 @@ function renderTimeline(acts) {
|
||||
// <span style="font-size:11px; color:#999">${formattedDate}</span>
|
||||
|
||||
return `
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
|
||||
<div class="timeline-content">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
|
||||
<b>${icon} ${formattedType}</b>
|
||||
<span style="font-size:11px; color:#999"> 👤 ${a.assigned_to_name} | 🗓 ${formattedDate}</span>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:2px;">
|
||||
<b style="white-space: nowrap; margin-right: 15px;">${icon} ${formattedType}</b>
|
||||
<span style="font-size:11px; color:#999; text-align:right;"> 👤 ${a.assigned_to_name} | 🗓️ ${formattedDate}</span>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; font-size:13px; color:#444;">
|
||||
|
||||
${a.additional_assigned_names ? `
|
||||
<div style="display:flex; justify-content:flex-end; margin-bottom:5px;">
|
||||
<span style="font-size:11px; color:#999; text-align: right;
|
||||
max-width: 75%; /* Forces long lists to wrap cleanly on the right side */
|
||||
display: inline-block;">
|
||||
👥 ${a.additional_assigned_names}
|
||||
</span>
|
||||
</div>
|
||||
` : '<div style="margin-bottom:8px;"></div>'}
|
||||
|
||||
<div style="font-size:13px; color:#444; margin-bottom: 8px;">
|
||||
${a.notes}
|
||||
</div>
|
||||
<div style="font-size:13px; color:#444;"></div>
|
||||
|
||||
${a.status === 'pending' ?
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:8px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
|
||||
`<div style="font-size:12px; color:#388e3c; margin-top:8px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:4px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
|
||||
`<div style="font-size:12px; color:#388e3c; margin-top:4px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</div>`;
|
||||
}
|
||||
function openActivityModal() {
|
||||
document.getElementById('act_owner').value = global_lead_assigned_to;
|
||||
document.getElementById('act_owner').dispatchEvent(new Event('change'));
|
||||
$(document.getElementById('act_owner')).trigger('change'); // tells Select2 to update display
|
||||
// document.getElementById('act_owner').dispatchEvent(new Event('change'));
|
||||
document.getElementById('act_lead_id').value = lead_id;
|
||||
openModal('activityModal');
|
||||
}
|
||||
@ -1004,7 +1089,8 @@ function openComp(id, assigned_to, lead_id) {
|
||||
document.getElementById('comp_id').value = id;
|
||||
document.getElementById('lead_id').value = lead_id;
|
||||
document.getElementById('f_assigned_to').value = assigned_to;
|
||||
document.getElementById('f_assigned_to').dispatchEvent(new Event('change'));
|
||||
$(document.getElementById('f_assigned_to')).trigger('change'); // tells Select2 to update display
|
||||
// document.getElementById('f_assigned_to').dispatchEvent(new Event('change'));
|
||||
document.getElementById('f_typeButtons').querySelectorAll('.f_activity_type')
|
||||
.forEach(btn => btn.classList.remove('active'));
|
||||
selectedFollowUpActivityType = '';
|
||||
@ -1250,11 +1336,14 @@ document.getElementById('editLeadForm').onsubmit = async (e) => {
|
||||
|
||||
if(res.ok) {
|
||||
toastr.success('Leads Updated Successfully');
|
||||
e.target.reset();
|
||||
closeModal('editLeadModal');
|
||||
fetchLeads();
|
||||
e.target.reset();
|
||||
$('#savedContactsContainer').empty();
|
||||
$('#NoData').show();
|
||||
// CLEANUP: Reset the contact UI for the next time it opens
|
||||
$('#contactPersonsList').find('.contact-card').remove();
|
||||
$('#noContactPersonsData').show();
|
||||
document.getElementById('editing_contact_id').value = '';
|
||||
document.getElementById('btnSaveContact').innerHTML = "✔ Save";
|
||||
} else {
|
||||
let err = await res.json();
|
||||
if (res.status === 400) {
|
||||
@ -1346,7 +1435,9 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
notes: actNotes, // Reused the trimmed variable from above
|
||||
scheduled_date: dbFormat,
|
||||
assigned_to: actOwner, // Reused the trimmed variable from above
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
additional_assigned_ids : Array.from(document.getElementById('act_multiple_owner').selectedOptions).map(o => o.value)
|
||||
|
||||
};
|
||||
|
||||
const res = await fetch(`${API}/activities`, {
|
||||
@ -1483,7 +1574,8 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
notes: document.getElementById('f_notes').value,
|
||||
scheduled_date: dbFormat,
|
||||
assigned_to: document.getElementById('f_assigned_to').value,
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
additional_assigned_ids : Array.from(document.getElementById('f_multiple_owner').selectedOptions).map(o => o.value)
|
||||
};
|
||||
|
||||
const res2 = await fetch(`${API}/activities`, {
|
||||
@ -1594,146 +1686,11 @@ document.getElementById('do_follow').addEventListener('change', function () {
|
||||
}
|
||||
});
|
||||
|
||||
const btnSaveContact = document.getElementById('btnSaveContact');
|
||||
if (btnSaveContact) {
|
||||
btnSaveContact.onclick = async (e) => {
|
||||
// Use .value instead of .val()
|
||||
let name = document.getElementById('contact_name').value.trim();
|
||||
let mobile = document.getElementById('contact_mobile').value.trim();
|
||||
let lead_id = document.getElementById('hidden_lead_id').value.trim();
|
||||
|
||||
if (!name || !mobile) {
|
||||
return toastr.warning('Please enter both contact person name and mobile number.');
|
||||
}
|
||||
|
||||
let payload = { lead_id: lead_id, name: name, mobile: mobile };
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/contacts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const contactResult = await res.json(); // Get the response body
|
||||
|
||||
if (res.ok) {
|
||||
// Clear inputs
|
||||
document.getElementById('contact_name').value = '';
|
||||
document.getElementById('contact_mobile').value = '';
|
||||
|
||||
// Hide "No Data" message
|
||||
const noData = document.getElementById('NoData');
|
||||
if(noData) noData.style.display = 'none';
|
||||
|
||||
// Create the HTML string
|
||||
let contactHtml = `
|
||||
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-3"
|
||||
style="background: #f8f8f8; border-radius: 8px;"
|
||||
data-id="${contactResult.data.contact_id}">
|
||||
<div>
|
||||
<div class="fw-bold">${contactResult.data.name}</div>
|
||||
<div class="text-muted">${contactResult.data.mobile}</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-danger btn-sm btnRemoveContact"
|
||||
data-id="${contactResult.data.contact_id}">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// <button type="button"
|
||||
// class="btn btn-secondary btn-sm btnEditContact"
|
||||
// data-id="${contactResult.data.contact_id}">
|
||||
// Update
|
||||
// </button>
|
||||
// Append to container using Vanilla JS
|
||||
document.getElementById('savedContactsContainer').insertAdjacentHTML('beforeend', contactHtml);
|
||||
toastr.success('Contact saved successfully');
|
||||
} else {
|
||||
|
||||
if (res.status === 400) {
|
||||
let errorMessages = "";
|
||||
let seenMessages = [];
|
||||
if (contactResult.messages) {
|
||||
Object.entries(contactResult.messages).forEach(([field, message]) => {
|
||||
let inputElement = $('[name="' + field + '"]');
|
||||
|
||||
if (inputElement.length > 0) {
|
||||
// Make the field box turn red so the user sees it immediately
|
||||
inputElement.addClass('is-invalid');
|
||||
|
||||
// Focus on the first error field
|
||||
if (isFirstError) {
|
||||
// The 100ms delay safely bypasses Bootstrap's modal focus block
|
||||
setTimeout(function() {
|
||||
inputElement.focus();
|
||||
}, 100);
|
||||
|
||||
isFirstError = false;
|
||||
}
|
||||
}
|
||||
if (!seenMessages.includes(message)) {
|
||||
errorMessages += `• ${message}<br>`;
|
||||
seenMessages.push(message);
|
||||
}
|
||||
});
|
||||
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
|
||||
} else {
|
||||
toastr.warning(contactResult.message || 'Validation failed', 'Warning');
|
||||
}
|
||||
}
|
||||
else {
|
||||
toastr.error(contactResult.message || 'Error adding lead');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toastr.error('An error occurred');
|
||||
}
|
||||
};
|
||||
}
|
||||
const savedContactsContainer = document.getElementById('savedContactsContainer');
|
||||
if (savedContactsContainer) {
|
||||
savedContactsContainer.onclick = async (e) => {
|
||||
|
||||
// Check if the clicked element is a Remove button
|
||||
if (e.target.classList.contains('btnRemoveContact')) {
|
||||
const btn = e.target;
|
||||
const contactId = btn.dataset.id; // Get data-id
|
||||
const card = btn.closest('.contact-card'); // Find the parent card
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/contacts/${contactId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
card.remove(); // Remove from DOM
|
||||
toastr.success('Contact removed successfully');
|
||||
|
||||
// Check if container is empty to show "No Data"
|
||||
const container = document.getElementById('savedContactsContainer');
|
||||
if (container.querySelectorAll('.contact-card').length === 0) {
|
||||
const noData = document.getElementById('NoData');
|
||||
if(noData) noData.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
toastr.error('Failed to remove contact');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toastr.error('An error occurred');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function openEditLeadModal(id) {
|
||||
// 1. Reset UI State
|
||||
document.getElementById('hidden_lead_id').value = id;
|
||||
const container = $('#savedContactsContainer');
|
||||
const container = $('#contactPersonsList');
|
||||
// Remove only previous contact cards, keep the NoData span for now
|
||||
container.find('.contact-card').remove();
|
||||
|
||||
@ -1757,35 +1714,49 @@ async function openEditLeadModal(id) {
|
||||
form.querySelector('[name="address"]').value = lead.address || '';
|
||||
form.querySelector('[name="website"]').value = lead.website || '';
|
||||
form.querySelector('[name="gst_number"]').value = lead.gst_number || '';
|
||||
form.querySelector('[name="status"]').value = lead.status || 'New';
|
||||
// form.querySelector('[name="status"]').value = lead.status || 'New';
|
||||
form.querySelector('[name="assigned_to"]').value = lead.assigned_to || '';
|
||||
form.querySelector('[name="assigned_to"]').value = lead.assigned_to || '';
|
||||
$(form.querySelector('[name="assigned_to"]')).trigger('change');
|
||||
const statusSelect = form.querySelector('[name="status"]');
|
||||
statusSelect.value = lead.status || 'New';
|
||||
updateStatusOptions(statusSelect.value);
|
||||
|
||||
// 4. Handle Contact Persons (Looping through the nested array)
|
||||
const contacts = lead.contact_persons; // Array from your JSON
|
||||
|
||||
if (contacts && contacts.length > 0) {
|
||||
$('#NoData').hide();
|
||||
|
||||
$('#noContactPersonsData').hide();
|
||||
contacts.forEach(contact => {
|
||||
const isPrimary = contact.is_primary == 1;
|
||||
const primaryBadge = isPrimary ? `<div style="margin-top:2px;"><span class="badge bg-warning text-dark ml-1" style="font-size:9px;">PRIMARY</span></div>` : '';
|
||||
|
||||
let contactHtml = `
|
||||
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-3"
|
||||
style="background: #f8f8f8; border-radius: 8px;"
|
||||
data-id="${contact.contact_id}">
|
||||
<div>
|
||||
<div class="fw-bold">${contact.name}</div>
|
||||
<div class="text-muted">${contact.mobile}</div>
|
||||
<div class="contact-card d-flex align-items-center mb-2 p-2 px-3"
|
||||
style="background: ${isPrimary ? '#fff9c4' : '#f8f8f8'}; border: 1px solid ${isPrimary ? '#fbc02d' : '#eee'}; border-radius: 8px; gap: 12px;">
|
||||
|
||||
<div style="flex: 2; min-width: 0;">
|
||||
<div class="fw-bold text-truncate" style="font-size:14px;">${contact.name} ${primaryBadge}</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-danger btn-sm btnRemoveContact"
|
||||
data-id="${contact.contact_id}">
|
||||
Remove
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
<div style="flex: 2; min-width: 0; color:#666; font-size:13px;">${contact.mobile}</div>
|
||||
<div style="flex: 2; min-width: 0; color:#666; font-size:13px;"><div class="text-truncate">${contact.designation}</div></div>
|
||||
|
||||
<div class="d-flex" style="gap:5px;">
|
||||
<button type="button" class="btnEditContact" data-id="${contact.contact_id}" data-info='${JSON.stringify(contact)}' title="Edit Contact Person"
|
||||
style="background:none; border:none; cursor:pointer; color:#1976d2; font-size:18px; flex-shrink:0; padding:4px 6px; border-radius:6px; transition:background 0.2s;"
|
||||
onmouseover="this.style.background='#e3f2fd'" onmouseout="this.style.background='none'">✏️</button>
|
||||
<button type="button" class="btnRemoveContact" data-id="${contact.contact_id}" title="Remove Contact Person"
|
||||
style="background:none; border:none; cursor:pointer; color:#1976d2; font-size:18px; flex-shrink:0; padding:4px 6px; border-radius:6px; transition:background 0.2s;"
|
||||
onmouseover="this.style.background='#ffebee'" onmouseout="this.style.background='none'">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.append(contactHtml);
|
||||
});
|
||||
} else {
|
||||
$('#NoData').show();
|
||||
}
|
||||
else {
|
||||
$('#noContactPersonsData').show();
|
||||
}
|
||||
|
||||
// 5. Open the Modal
|
||||
@ -1797,6 +1768,125 @@ async function openEditLeadModal(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 1. HANDLE EDIT & REMOVE (Event Delegation) ---
|
||||
document.getElementById('contactPersonsList').onclick = async (e) => {
|
||||
const btnEdit = e.target.closest('.btnEditContact');
|
||||
const btnRemove = e.target.closest('.btnRemoveContact');
|
||||
|
||||
// EDIT LOGIC
|
||||
if (btnEdit) {
|
||||
const card = btnEdit.closest('.contact-card');
|
||||
const contactData = JSON.parse(btnEdit.dataset.info);
|
||||
|
||||
// Fill Form
|
||||
document.getElementById('editing_contact_id').value = contactData.contact_id;
|
||||
document.getElementById('contact_name').value = contactData.name;
|
||||
document.getElementById('contact_mobile').value = contactData.mobile;
|
||||
document.getElementById('contact_designation').value = contactData.designation;
|
||||
document.getElementById('contact_is_primary').checked = contactData.is_primary == 1;
|
||||
|
||||
// Change Button UI
|
||||
const saveBtn = document.getElementById('btnSaveContact');
|
||||
saveBtn.innerHTML = "Update";
|
||||
|
||||
document.getElementById('contact_name').focus();
|
||||
}
|
||||
|
||||
// REMOVE LOGIC
|
||||
if (btnRemove) {
|
||||
const contactId = btnRemove.dataset.id;
|
||||
if (!confirm('Are you sure you want to remove this contact?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/contacts/${contactId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
toastr.success('Removed successfully');
|
||||
openEditLeadModal(document.getElementById('hidden_lead_id').value); // Refresh
|
||||
}
|
||||
} catch (err) { console.error(err); }
|
||||
}
|
||||
};
|
||||
|
||||
// --- 2. SAVE / UPDATE LOGIC ---
|
||||
const btnSaveContact = document.getElementById('btnSaveContact');
|
||||
if (btnSaveContact) {
|
||||
btnSaveContact.onclick = async (e) => {
|
||||
const editingId = document.getElementById('editing_contact_id').value;
|
||||
const leadId = document.getElementById('hidden_lead_id').value;
|
||||
|
||||
let payload = {
|
||||
lead_id: leadId,
|
||||
name: document.getElementById('contact_name').value.trim(),
|
||||
mobile: document.getElementById('contact_mobile').value.trim(),
|
||||
designation: document.getElementById('contact_designation').value.trim(),
|
||||
is_primary: document.getElementById('contact_is_primary').checked ? 1 : 0
|
||||
};
|
||||
|
||||
if (!payload.name || !payload.mobile) {
|
||||
return toastr.warning('Name and Mobile are required');
|
||||
}
|
||||
|
||||
// Determine if we POST (new) or PUT (edit)
|
||||
const url = editingId ? `${API}/contacts/${editingId}` : `${API}/contacts`;
|
||||
const method = editingId ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(editingId ? 'Contact Updated' : 'Contact Saved');
|
||||
|
||||
// Reset Form UI
|
||||
document.getElementById('editing_contact_id').value = '';
|
||||
document.getElementById('contact_name').value = '';
|
||||
document.getElementById('contact_mobile').value = '';
|
||||
document.getElementById('contact_designation').value = '';
|
||||
document.getElementById('contact_is_primary').checked = false;
|
||||
|
||||
const saveBtn = document.getElementById('btnSaveContact');
|
||||
saveBtn.innerHTML = "✔ Save";
|
||||
// Refresh List
|
||||
openEditLeadModal(leadId);
|
||||
} else {
|
||||
const err = await res.json();
|
||||
toastr.error(err.message || 'Error processing request');
|
||||
}
|
||||
} catch (error) { console.error(error); }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const statusOrder = ['New', 'Potential', 'Prospects', 'Not a Prospects'];
|
||||
|
||||
function updateStatusOptions(currentStatus) {
|
||||
|
||||
let Eform = document.getElementById('editLeadForm');
|
||||
if (!Eform) return; // safety check
|
||||
|
||||
|
||||
let select = Eform.querySelector('[name="status"]');
|
||||
let currentIndex = statusOrder.indexOf(currentStatus);
|
||||
|
||||
// First show all
|
||||
select.querySelectorAll('option').forEach(opt => {
|
||||
opt.hidden = false;
|
||||
});
|
||||
|
||||
// Hide previous ones
|
||||
if (currentIndex > -1) {
|
||||
statusOrder.slice(0, currentIndex).forEach(status => {
|
||||
const opt = select.querySelector(`option[value="${status}"]`);
|
||||
if (opt) opt.hidden = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function convertDBFormatted(input) {
|
||||
|
||||
if (!input) return null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user