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\LeadFilesModel;
|
||||||
use App\Models\LeadInstallmentPaymentDetails;
|
use App\Models\LeadInstallmentPaymentDetails;
|
||||||
use App\Models\GmailSentHistoryModel;
|
use App\Models\GmailSentHistoryModel;
|
||||||
|
use App\Models\SalesActualLeadModel;
|
||||||
|
use App\Models\SalesContactPersonModel;
|
||||||
|
|
||||||
use App\Helpers\MailHelper;
|
use App\Helpers\MailHelper;
|
||||||
use App\Helpers\ExcelMergeHelper;
|
use App\Helpers\ExcelMergeHelper;
|
||||||
@ -83,6 +85,8 @@ class LeadsController extends BaseController
|
|||||||
protected $buisnessType;
|
protected $buisnessType;
|
||||||
protected $member_data_excel_columns;
|
protected $member_data_excel_columns;
|
||||||
protected $general_relationships;
|
protected $general_relationships;
|
||||||
|
protected $leadModel;
|
||||||
|
protected $contactModel;
|
||||||
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
@ -107,6 +111,8 @@ class LeadsController extends BaseController
|
|||||||
$this->leadFilesModel = new LeadFilesModel();
|
$this->leadFilesModel = new LeadFilesModel();
|
||||||
$this->leadInstallmentPaymentDetails = new LeadInstallmentPaymentDetails();
|
$this->leadInstallmentPaymentDetails = new LeadInstallmentPaymentDetails();
|
||||||
$this->gmailSentHistoryModel = new GmailSentHistoryModel();
|
$this->gmailSentHistoryModel = new GmailSentHistoryModel();
|
||||||
|
$this->leadModel = new SalesActualLeadModel();
|
||||||
|
$this->contactModel = new SalesContactPersonModel();
|
||||||
|
|
||||||
$this->issuer = [1 => 'JIBS', 2 => 'Nhance'];
|
$this->issuer = [1 => 'JIBS', 2 => 'Nhance'];
|
||||||
$this->clientType = [1 => 'Group', 2 => 'Individual'];
|
$this->clientType = [1 => 'Group', 2 => 'Individual'];
|
||||||
@ -627,6 +633,7 @@ class LeadsController extends BaseController
|
|||||||
{
|
{
|
||||||
|
|
||||||
$request_data = $this->request->getPost();
|
$request_data = $this->request->getPost();
|
||||||
|
print_r($request_data);die;
|
||||||
$data = sanitizeInputArrayAdvanced($request_data);
|
$data = sanitizeInputArrayAdvanced($request_data);
|
||||||
|
|
||||||
$data['client_type'] = 1;
|
$data['client_type'] = 1;
|
||||||
@ -825,6 +832,7 @@ class LeadsController extends BaseController
|
|||||||
$last_3_years_claims = $data['finyear'];
|
$last_3_years_claims = $data['finyear'];
|
||||||
|
|
||||||
$processedData[] = [
|
$processedData[] = [
|
||||||
|
'lost_reason' => $data['lost_reason'] ?? null,
|
||||||
'actual_lead_id' => $data['actual_lead_id'] ?? null,
|
'actual_lead_id' => $data['actual_lead_id'] ?? null,
|
||||||
'lead_type' => $data['lead_type'],
|
'lead_type' => $data['lead_type'],
|
||||||
'issuer' => $data['issuer'],
|
'issuer' => $data['issuer'],
|
||||||
@ -4372,6 +4380,28 @@ class LeadsController extends BaseController
|
|||||||
'actual_lead_id' => $actual_lead_id,
|
'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
|
// Fetch sales team members who are active in team 5
|
||||||
$data['salse_team'] = $this->userModel
|
$data['salse_team'] = $this->userModel
|
||||||
->select('user_profiles.*')
|
->select('user_profiles.*')
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -47,6 +47,7 @@ class LeadsModel extends Model
|
|||||||
'proposel_data',
|
'proposel_data',
|
||||||
'status',
|
'status',
|
||||||
'notes',
|
'notes',
|
||||||
|
'lost_reason',
|
||||||
'created_at',
|
'created_at',
|
||||||
'created_by',
|
'created_by',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class SalesActivityModel extends Model
|
|||||||
'completion_notes',
|
'completion_notes',
|
||||||
'completed_date',
|
'completed_date',
|
||||||
'parent_activity_id',
|
'parent_activity_id',
|
||||||
|
'additional_assigned_ids',
|
||||||
'created_by',
|
'created_by',
|
||||||
'updated_by'
|
'updated_by'
|
||||||
];
|
];
|
||||||
@ -64,8 +65,14 @@ class SalesActivityModel extends Model
|
|||||||
*/
|
*/
|
||||||
public function getActivitiesByLead($leadId, $status = null)
|
public function getActivitiesByLead($leadId, $status = null)
|
||||||
{
|
{
|
||||||
$builder = $this->select('sales_activities.*, user_profiles.first_name as assigned_to_name')
|
// Convert the INT id to a string, then wrap it in JSON quotes to match ["5", "11"]
|
||||||
->join('user_profiles', 'user_profiles.id = sales_activities.assigned_to', 'left')
|
$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);
|
->where('sales_activities.lead_id', $leadId);
|
||||||
|
|
||||||
if ($status) {
|
if ($status) {
|
||||||
@ -78,7 +85,7 @@ class SalesActivityModel extends Model
|
|||||||
/**
|
/**
|
||||||
* Get all sales_activities with filters
|
* 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')
|
$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')
|
->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
|
* Complete an activity
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -123,7 +123,38 @@ class SalesActualLeadModel extends Model
|
|||||||
|
|
||||||
$data = $this->findAll($limit, $offset);
|
$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>
|
</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-row">
|
||||||
<div class="form-group col-md-12">
|
<div class="form-group col-md-12">
|
||||||
<label for="notes">Remarks</label>
|
<label for="notes">Remarks</label>
|
||||||
@ -778,7 +784,8 @@
|
|||||||
$('#client_name').val(res.data.client_name);
|
$('#client_name').val(res.data.client_name);
|
||||||
$('#client_short_name').val(res.data.client_short_name);
|
$('#client_short_name').val(res.data.client_short_name);
|
||||||
$('#entity_type_id').val(res.data.entity_type_id);
|
$('#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);
|
$('#notes').val(res.data.notes);
|
||||||
|
|
||||||
setTimeout(function() {
|
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>
|
</script>
|
||||||
@ -410,6 +410,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-row">
|
||||||
<div class="form-group col-md-12">
|
<div class="form-group col-md-12">
|
||||||
<label for="notes">Remarks</label>
|
<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>
|
</script>
|
||||||
@ -10,6 +10,8 @@
|
|||||||
.btn-complete:hover { background: #4caf50; transform: translateY(-1px); }
|
.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 { 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-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 */
|
||||||
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
.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;}
|
.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; }
|
.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-new { background: #e3f2fd; color: #1976d2; }
|
||||||
.status-potential { background: #fff3e0; color: #f57c00; }
|
.status-potential { background: #fff3e0; color: #f57c00; }
|
||||||
@ -76,6 +79,10 @@
|
|||||||
.status-pending { background: #fff3e0; color: #f57c00; }
|
.status-pending { background: #fff3e0; color: #f57c00; }
|
||||||
.status-completed { background: #e8f5e9; color: #388e3c; }
|
.status-completed { background: #e8f5e9; color: #388e3c; }
|
||||||
.status-unknown { background: #000; color: #fff; }
|
.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 */
|
/* 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; }
|
.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-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;}
|
||||||
.opportunity-detail-item { font-size: 13px; color: #666;}
|
.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; }
|
.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 */
|
/* 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; }
|
.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;
|
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>
|
</style>
|
||||||
|
|
||||||
@ -159,7 +184,7 @@
|
|||||||
<!-- RIGHT SIDE -->
|
<!-- RIGHT SIDE -->
|
||||||
<div class="lead-actions">
|
<div class="lead-actions">
|
||||||
<input type="text" class="search-input" id="mainSearch"
|
<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()">
|
<button class="btn-primary" onclick="openMainActivityModal()">
|
||||||
+ Add Activity
|
+ Add Activity
|
||||||
</button>
|
</button>
|
||||||
@ -191,7 +216,7 @@
|
|||||||
<h2 id="det_company">Lead Detail</h2>
|
<h2 id="det_company">Lead Detail</h2>
|
||||||
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -233,7 +258,7 @@
|
|||||||
<div class="modal-content" style="max-width: 600px;">
|
<div class="modal-content" style="max-width: 600px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Add Activity</h2>
|
<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>
|
</div>
|
||||||
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
|
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
@ -276,8 +301,16 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||||||
<select id="act_owner" class="search-input searchable" style="width:100%" required>
|
<select id="act_owner" class="search-input searchable" style="width:100%" required>
|
||||||
<?php foreach($users as $user): ?>
|
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
<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; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -297,7 +330,7 @@
|
|||||||
<div class="modal-content" style="max-width: 675px;">
|
<div class="modal-content" style="max-width: 675px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>Complete Activity</h3>
|
<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>
|
</div>
|
||||||
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
|
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
@ -346,8 +379,16 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||||||
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
|
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
|
||||||
<?php foreach($users as $user): ?>
|
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
<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; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -368,7 +409,7 @@
|
|||||||
<div class="modal-content" style="max-width: 600px;">
|
<div class="modal-content" style="max-width: 600px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Select Opportunity Type</h2>
|
<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>
|
</div>
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
<div class="modal-body p-4">
|
<div class="modal-body p-4">
|
||||||
@ -401,17 +442,44 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
resetFlatpicker();
|
resetFlatpicker();
|
||||||
$('.searchable').each(function() {
|
|
||||||
let parentModal = $(this).closest('.modal');
|
// Single selects
|
||||||
$(this).select2({
|
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||||||
|
let parentModal = el.closest('.modal');
|
||||||
|
$(el).select2({
|
||||||
placeholder: "Select..",
|
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(){
|
function resetFlatpicker(){
|
||||||
document.querySelectorAll(".forschedule").forEach(el => {
|
document.querySelectorAll(".forschedule").forEach(el => {
|
||||||
@ -438,7 +506,7 @@ const activityIcons = {
|
|||||||
"Share Docs": "📄",
|
"Share Docs": "📄",
|
||||||
"To Do": "✓"
|
"To Do": "✓"
|
||||||
};
|
};
|
||||||
const salesManagerIds = <?= json_encode($sales_manager_ids ?? []) ?>;
|
const salesManagerWithHeadIds = <?= json_encode($sales_manager_with_head_ids ?? []) ?>;
|
||||||
|
|
||||||
const API = '<?= base_url('sales') ?>';
|
const API = '<?= base_url('sales') ?>';
|
||||||
let filter = 'all';
|
let filter = 'all';
|
||||||
@ -535,10 +603,10 @@ async function fetchActivities(isLoadMore = false) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
console.log("function here");
|
console.log("function here");
|
||||||
console.log(salesManagerIds);
|
console.log(salesManagerWithHeadIds);
|
||||||
// return;
|
// return;
|
||||||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
// 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.");
|
console.log("No Sales Manager IDs found. Skipping API call.");
|
||||||
grid.innerHTML = emptyStateHTML;
|
grid.innerHTML = emptyStateHTML;
|
||||||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||||||
@ -565,8 +633,8 @@ async function fetchActivities(isLoadMore = false) {
|
|||||||
// 5. Build URL with dynamic offset
|
// 5. Build URL with dynamic offset
|
||||||
let url = `${API}/activities?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
|
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!
|
// url += `&assigned_to=${salesManagerIds.join(',')}`; // We already proved it exists above!
|
||||||
if (typeof salesManagerIds !== 'undefined' && salesManagerIds.length > 0) {
|
if (typeof salesManagerWithHeadIds !== 'undefined' && salesManagerWithHeadIds.length > 0) {
|
||||||
url += `&assigned_to=${salesManagerIds.join(',')}`;
|
url += `&assigned_to=${salesManagerWithHeadIds.join(',')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Fetching API:", url);
|
console.log("Fetching API:", url);
|
||||||
@ -579,6 +647,15 @@ async function fetchActivities(isLoadMore = false) {
|
|||||||
const totalrecords = json.total || 0;
|
const totalrecords = json.total || 0;
|
||||||
console.log("API Response:", activityData);
|
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
|
const html = activityData
|
||||||
.sort((a, b) => Number(b.activity_id) - Number(a.activity_id))
|
.sort((a, b) => Number(b.activity_id) - Number(a.activity_id))
|
||||||
.map(a => {
|
.map(a => {
|
||||||
@ -613,9 +690,18 @@ async function fetchActivities(isLoadMore = false) {
|
|||||||
<div class="activity-lead-name">${a.company_name}</div>
|
<div class="activity-lead-name">${a.company_name}</div>
|
||||||
<div class="activity-notes" title="${displayNote}">${displayNote}</div>
|
<div class="activity-notes" title="${displayNote}">${displayNote}</div>
|
||||||
<div class="activity-meta">
|
<div class="activity-meta">
|
||||||
<span>📅 ${formattedCreatedDate}</span>
|
<span>🗓️ ${formattedCreatedDate}</span>
|
||||||
<span>👤 ${a.assigned_to_name}</span>
|
<span>👤 ${a.assigned_to_name}</span>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="activity-actions">
|
<div class="activity-actions">
|
||||||
@ -706,10 +792,17 @@ async function viewDetail(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderCard(opps) {
|
function renderCard(opps) {
|
||||||
const cont = document.getElementById('opportunitiesContainer');
|
const opp_cont = document.getElementById('opportunitiesContainer');
|
||||||
|
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||||
cont.innerHTML = opps.length ? opps.map(o => {
|
|
||||||
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
|
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', {
|
let formattedDate = new Date(o.created_at).toLocaleString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@ -736,11 +829,21 @@ function renderCard(opps) {
|
|||||||
<strong>Created At:</strong> ${formattedDate}
|
<strong>Created At:</strong> ${formattedDate}
|
||||||
</div>
|
</div>
|
||||||
<div class="opportunity-detail-item">
|
<div class="opportunity-detail-item">
|
||||||
<strong>Status:</strong> ${o.status}
|
<strong>Status:</strong> <span class="${statusClass}">${opp_status_value}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="opportunity-footer">
|
<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>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -777,21 +880,32 @@ function renderTimeline(acts) {
|
|||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
|
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
|
|
||||||
<b>${icon} ${formattedType}</b>
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:2px;">
|
||||||
<span style="font-size:11px; color:#999"> 👤 ${a.assigned_to_name} | 🗓 ${formattedDate}</span>
|
<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>
|
||||||
<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}
|
${a.notes}
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:13px; color:#444;"></div>
|
|
||||||
${a.status === 'pending' ?
|
${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>` :
|
`<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:8px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
`<div style="font-size:12px; color:#388e3c; margin-top:4px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>`;
|
||||||
`;
|
|
||||||
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</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
|
notes: actNotes, // Reused the trimmed variable from above
|
||||||
scheduled_date: dbFormat,
|
scheduled_date: dbFormat,
|
||||||
assigned_to: actOwner, // Reused the trimmed variable from above
|
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`, {
|
const res = await fetch(`${API}/activities`, {
|
||||||
@ -1023,7 +1138,8 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
|||||||
notes: document.getElementById('f_notes').value,
|
notes: document.getElementById('f_notes').value,
|
||||||
scheduled_date: dbFormat,
|
scheduled_date: dbFormat,
|
||||||
assigned_to: document.getElementById('f_assigned_to').value,
|
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`, {
|
const res2 = await fetch(`${API}/activities`, {
|
||||||
|
|||||||
@ -1,63 +1,110 @@
|
|||||||
<style>
|
<style>
|
||||||
.dash-container { padding: 25px; background: #f8f9fa; font-family: 'Segoe UI', sans-serif; }
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||||||
.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; }
|
.dash-container {
|
||||||
.card { background: white; padding: 25px; border-radius: 12px; border: 1px solid #edf2f7; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
|
padding: 28px;
|
||||||
.stat-val { font-size: 28px; font-weight: 700; color: #1a202c; }
|
background: #f4f6fb;
|
||||||
.stat-label { color: #718096; font-size: 14px; margin-top: 4px; font-weight: 500; }
|
font-family: 'Plus Jakarta Sans', 'Segoe UI', sans-serif;
|
||||||
.stat-change { font-size: 11px; margin-top: 8px; font-weight: 600; }
|
min-height: 100vh;
|
||||||
.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 */
|
/* ── Dashboard Header ── */
|
||||||
|
.dash-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title { font-size: 20px; font-weight: 800; color: #0f172a; margin: 0 0 4px 0; }
|
||||||
|
.page-sub { font-size: 13px; color: #7c8db0; font-weight: 600; }
|
||||||
|
.fy-select-wrap select {
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e4eaf5;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
min-width: 200px;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%237c8db0' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 14px center;
|
||||||
|
padding-right: 36px;
|
||||||
|
}
|
||||||
|
.fy-select-wrap select:focus { outline: none; border-color: #4f46e5; }
|
||||||
|
|
||||||
|
/* ── Stat Cards ── */
|
||||||
|
.stat-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.card-hero {
|
||||||
|
background: white;
|
||||||
|
padding: 22px 24px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid #e8edf5;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
.card-hero::before {
|
.card-hero::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0; left: 0;
|
||||||
left: 0;
|
width: 100%; height: 4px;
|
||||||
width: 100%;
|
|
||||||
height: 4px; /* Thin line at the top */
|
|
||||||
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
|
||||||
}
|
}
|
||||||
|
.card-hero:nth-child(1)::before { background: linear-gradient(90deg, #667eea, #764ba2); }
|
||||||
|
.card-hero:nth-child(2)::before { background: linear-gradient(90deg, #f093fb, #f5576c); }
|
||||||
|
.card-hero:nth-child(3)::before { background: linear-gradient(90deg, #fda085, #f6d365); }
|
||||||
|
.card-hero:nth-child(4)::before { background: linear-gradient(90deg, #43e97b, #38f9d7); }
|
||||||
|
.card-hero:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.09); }
|
||||||
|
.stat-icon { width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; margin-bottom: 14px; }
|
||||||
|
.stat-val { font-size: 30px; font-weight: 800; color: #1a202c; line-height: 1; }
|
||||||
|
.stat-label { color: #8896aa; font-size: 13px; margin-top: 6px; font-weight: 600; }
|
||||||
|
|
||||||
.card-hero:hover {
|
/* ── Shared Card ── */
|
||||||
transform: translateY(-5px);
|
.card { background: white; padding: 24px; border-radius: 14px; border: 1px solid #e8edf5; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
|
||||||
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optional: Subtle Background Gradient */
|
/* ── Main Grid ── */
|
||||||
.card-hero.gradient-bg {
|
.main-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 24px; align-items: start; }
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
|
.main-grid > .card { height: 520px; display: flex; flex-direction: column; }
|
||||||
}
|
.table-scroll-area { flex: 1; overflow-y: auto; }
|
||||||
|
.table-scroll-area thead th { position: sticky; top: 0; background: white; z-index: 1; }
|
||||||
|
|
||||||
.main-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 30px; }
|
/* ── Right Panel ── */
|
||||||
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
.right-panel { height: 520px; background: white; border-radius: 14px; border: 1px solid #e8edf5; box-shadow: 0 2px 8px rgba(0,0,0,0.04); padding: 22px; box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
.team-members-scroll { flex: 1; overflow-y: auto; margin-top: 10px; }
|
||||||
.team-member { display: flex; align-items: center; padding: 15px 0; border-bottom: 1px solid #f1f5f9; }
|
.team-member { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid #f4f6fb; }
|
||||||
.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; }
|
.team-member:last-child { border-bottom: none; }
|
||||||
|
.member-img { width: 38px; height: 38px; border-radius: 50%; color: white; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 15px; flex-shrink: 0; }
|
||||||
.act-stats { text-align: right; }
|
.act-stats { text-align: right; }
|
||||||
.act-stats .total { font-weight: 700; font-size: 13px; color: #1a202c; }
|
.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; }
|
.act-stats .done { font-size: 11px; color: #38a169; font-weight: 600; margin-top: 2px; }
|
||||||
|
|
||||||
.breakdown-card { margin-top: 20px; }
|
/* ── Activity Breakdown ── */
|
||||||
.breakdown-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 13px; }
|
.breakdown-section { flex-shrink: 0; margin-top: 12px; border: 1px solid #f0f4fa; border-radius: 12px; padding: 14px; background: #fafbfd; }
|
||||||
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 8px; }
|
.breakdown-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 9px; font-size: 13px; }
|
||||||
|
.breakdown-item:last-child { margin-bottom: 0; }
|
||||||
|
.breakdown-bar-wrap { flex: 1; height: 5px; background: #eef0f5; border-radius: 99px; margin: 0 10px; overflow: hidden; }
|
||||||
|
.breakdown-bar { height: 100%; border-radius: 99px; }
|
||||||
|
|
||||||
|
/* ── Table ── */
|
||||||
|
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||||
table { width: 100%; border-collapse: collapse; }
|
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; }
|
th { text-align: left; padding: 10px 12px; color: #8896aa; font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; border-bottom: 1px solid #edf2f7; background: white; font-weight: 700; }
|
||||||
td { padding: 15px 12px; border-bottom: 1px solid #f1f5f9; font-size: 14px; vertical-align: middle; }
|
td { padding: 13px 12px; border-bottom: 1px solid #f4f6fb; font-size: 14px; vertical-align: middle; }
|
||||||
|
tbody tr:hover { background: #fafbfe; }
|
||||||
|
|
||||||
|
/* ── Status Pills ── */
|
||||||
.status-pill { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; }
|
.status-pill { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; }
|
||||||
.status-completed { background: #f0fff4; color: #38a169; }
|
.status-completed { background: #f0fff4; color: #38a169; }
|
||||||
.status-pending { background: #fffaf0; color: #dd6b20; }
|
.status-pending { background: #fffaf0; color: #dd6b20; }
|
||||||
@ -65,62 +112,205 @@
|
|||||||
.status-potential { background: #fff3e0; color: #f57c00; }
|
.status-potential { background: #fff3e0; color: #f57c00; }
|
||||||
.status-prospects { background: #e8f5e9; color: #388e3c; }
|
.status-prospects { background: #e8f5e9; color: #388e3c; }
|
||||||
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
||||||
|
.leads-table-wrap { max-height: 480px; overflow-y: auto; }
|
||||||
|
.leads-table-wrap thead th { position: sticky; top: 0; background: white; z-index: 1; }
|
||||||
|
.empty-state { text-align: center; padding: 50px 20px; color: #aaa; }
|
||||||
|
.empty-icon { width: 70px; height: 70px; margin: 0 auto 16px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32px; }
|
||||||
|
|
||||||
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
|
/* ── Responsive ── */
|
||||||
.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; }
|
@media (max-width: 1200px) { .stat-cards { grid-template-columns: repeat(2, 1fr); } }
|
||||||
|
@media (max-width: 992px) { .main-grid { grid-template-columns: 1fr; } .main-grid > .card, .right-panel { height: 450px; } .stat-cards { grid-template-columns: repeat(2, 1fr); } }
|
||||||
|
@media (max-width: 576px) { .dash-container { padding: 14px; } .stat-cards { grid-template-columns: 1fr 1fr; gap: 10px; } .stat-val { font-size: 24px; } .main-grid > .card, .right-panel { height: 400px; } table th, table td { padding: 10px 8px; font-size: 12px; } .dash-header { flex-direction: column; align-items: flex-start; } }
|
||||||
|
|
||||||
|
/* ══ TEAM ACHIEVEMENT SECTION ══ */
|
||||||
|
.section-head { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 16px; }
|
||||||
|
.section-title { font-size: 16px; font-weight: 800; color: #0f172a; }
|
||||||
|
.section-sub { font-size: 12px; color: #7c8db0; font-weight: 600; margin-top: 3px; }
|
||||||
|
.branch-summary { text-align: right; }
|
||||||
|
.branch-amt { font-size: 20px; font-weight: 800; color: #0f172a; }
|
||||||
|
.branch-bar-wrap { width: 180px; height: 5px; background: #e4eaf5; border-radius: 99px; overflow: hidden; margin: 6px 0 4px auto; }
|
||||||
|
.branch-bar-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg,#4f46e5,#818cf8); }
|
||||||
|
.branch-pct-lbl { font-size: 11px; color: #4f46e5; font-weight: 700; }
|
||||||
|
.achieve-list { background: #fff; border-radius: 14px; border: 1px solid #e4eaf5; box-shadow: 0 2px 12px rgba(15,23,42,0.06); overflow: hidden; }
|
||||||
|
.list-col-head { display: grid; grid-template-columns: 260px 1fr 130px 130px 130px 110px 80px; padding: 10px 22px; background: #f8fafd; border-bottom: 1px solid #e4eaf5; }
|
||||||
|
.col-lbl { font-size: 10px; font-weight: 700; color: #7c8db0; letter-spacing: .07em; text-transform: uppercase; display: flex; align-items: center; }
|
||||||
|
.col-lbl.right { justify-content: flex-end; }
|
||||||
|
.col-lbl.center { justify-content: center; }
|
||||||
|
.achieve-row { display: grid; grid-template-columns: 260px 1fr 130px 130px 130px 110px 80px; padding: 16px 22px; border-bottom: 1px solid #f1f5f9; align-items: center; cursor: pointer; transition: background .15s; position: relative; }
|
||||||
|
.achieve-row:last-child { border-bottom: none; }
|
||||||
|
.achieve-row:hover { background: #f8fafd; }
|
||||||
|
.achieve-row:hover .row-arrow { opacity: 1; transform: translateY(-50%) translateX(0); }
|
||||||
|
.member-cell { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.m-avatar { width: 42px; height: 42px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 16px; color: #fff; flex-shrink: 0; }
|
||||||
|
.m-name { font-weight: 700; font-size: 14px; color: #0f172a; }
|
||||||
|
.m-role { font-size: 11px; color: #7c8db0; font-weight: 600; margin-top: 2px; }
|
||||||
|
.prog-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; }
|
||||||
|
.prog-pct { font-size: 13px; font-weight: 800; }
|
||||||
|
.prog-amts { font-size: 11px; color: #7c8db0; font-weight: 600; }
|
||||||
|
.prog-bar-wrap { height: 6px; background: #f0f3fa; border-radius: 99px; overflow: hidden; }
|
||||||
|
.prog-bar { height: 100%; border-radius: 99px; }
|
||||||
|
.num-cell { text-align: right; }
|
||||||
|
.num-val { font-size: 15px; font-weight: 800; }
|
||||||
|
.num-lbl { font-size: 10px; color: #7c8db0; font-weight: 700; margin-top: 2px; letter-spacing: .04em; }
|
||||||
|
.num-cell.center-col { text-align: center; }
|
||||||
|
.badge { display: inline-flex; align-items: center; gap: 5px; padding: 4px 12px; border-radius: 99px; font-size: 11px; font-weight: 700; white-space: nowrap; }
|
||||||
|
.b-achieved { background: #ecfdf5; color: #059669; }
|
||||||
|
.b-ontrack { background: #eff6ff; color: #2563eb; }
|
||||||
|
.b-behind { background: #fff7ed; color: #c2410c; }
|
||||||
|
.b-atrisk { background: #fef2f2; color: #b91c1c; }
|
||||||
|
.row-arrow { position: absolute; right: 18px; top: 50%; transform: translateY(-50%) translateX(4px); color: #cbd5e1; font-size: 16px; opacity: 0; transition: opacity .15s, transform .15s; }
|
||||||
|
|
||||||
|
/* ══ MODAL ══ */
|
||||||
|
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(10,15,30,0.55); backdrop-filter: blur(6px); z-index: 1000; align-items: center; justify-content: center; padding: 20px; }
|
||||||
|
.modal-overlay.open { display: flex; animation: fadeIn .2s ease; }
|
||||||
|
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
|
||||||
|
.modal-box { background: #fff; border-radius: 20px; width: 100%; max-width: 780px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 32px 80px rgba(15,23,42,0.25); animation: slideUp .25s ease; }
|
||||||
|
@keyframes slideUp { from { transform: translateY(30px); opacity: 0 } to { transform: translateY(0); opacity: 1 } }
|
||||||
|
.modal-header { padding: 20px 24px 16px; border-bottom: 1px solid #e4eaf5; display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
|
||||||
|
.modal-ava { width: 50px; height: 50px; border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 21px; font-weight: 800; color: #fff; flex-shrink: 0; }
|
||||||
|
.modal-name { font-size: 17px; font-weight: 800; }
|
||||||
|
.modal-fy-badge { font-size: 11px; font-weight: 700; background: #eff6ff; color: #2563eb; padding: 3px 10px; border-radius: 99px; margin-top: 4px; display: inline-block; }
|
||||||
|
.modal-close { margin-left: auto; background: #f1f5f9; border: none; width: 34px; height: 34px; border-radius: 50%; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; color: #7c8db0; transition: background .2s; }
|
||||||
|
.modal-close:hover { background: #e2e8f0; color: #0f172a; }
|
||||||
|
.modal-summary { display: grid; grid-template-columns: repeat(4,1fr); background: #f8fafd; border-bottom: 1px solid #e4eaf5; flex-shrink: 0; }
|
||||||
|
.ms-box { padding: 14px 18px; text-align: center; border-right: 1px solid #e4eaf5; }
|
||||||
|
.ms-box:last-child { border-right: none; }
|
||||||
|
.ms-num { font-size: 19px; font-weight: 800; }
|
||||||
|
.ms-lbl { font-size: 10px; color: #7c8db0; font-weight: 700; letter-spacing: .05em; margin-top: 2px; }
|
||||||
|
.modal-body { padding: 22px 24px; overflow-y: auto; flex: 1; }
|
||||||
|
|
||||||
|
/* ── Modal Tabs ── */
|
||||||
|
.modal-tabs { display: flex; gap: 4px; background: #f1f5f9; border-radius: 10px; padding: 4px; margin-bottom: 20px; }
|
||||||
|
.modal-tab { flex: 1; padding: 9px 16px; border: none; border-radius: 8px; font-size: 13px; font-weight: 700; cursor: pointer; background: transparent; color: #7c8db0; transition: all .2s; }
|
||||||
|
.modal-tab.active { background: #fff; color: #0f172a; box-shadow: 0 2px 8px rgba(15,23,42,0.08); }
|
||||||
|
.modal-tab-panel { display: none; }
|
||||||
|
.modal-tab-panel.active { display: block; }
|
||||||
|
|
||||||
|
.overall-prog { background: linear-gradient(135deg,#0f172a,#1e293b); border-radius: 14px; padding: 18px 20px; color: #fff; margin-bottom: 20px; }
|
||||||
|
.op-label { font-size: 10px; font-weight: 700; letter-spacing: .08em; color: #94a3b8; }
|
||||||
|
.op-row { display: flex; justify-content: space-between; align-items: flex-end; margin: 6px 0 12px; }
|
||||||
|
.op-achieved { font-size: 24px; font-weight: 800; }
|
||||||
|
.op-target { font-size: 12px; color: #94a3b8; font-weight: 600; }
|
||||||
|
.op-pct { font-size: 26px; font-weight: 800; color: #818cf8; }
|
||||||
|
.op-bar-wrap { background: rgba(255,255,255,0.12); height: 7px; border-radius: 99px; overflow: hidden; }
|
||||||
|
.op-bar-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg,#818cf8,#67e8f9); }
|
||||||
|
.op-foot { display: flex; justify-content: space-between; font-size: 11px; color: #94a3b8; font-weight: 600; margin-top: 7px; }
|
||||||
|
.split-section-title { font-size: 13px; font-weight: 800; margin-bottom: 12px; }
|
||||||
|
.split-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
.split-table thead th { background: #f8fafd; padding: 9px 12px; text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .07em; color: #7c8db0; font-weight: 500; border-bottom: 2px solid #e4eaf5; }
|
||||||
|
.split-table thead th:last-child { text-align: right; }
|
||||||
|
.split-table tbody tr { border-bottom: 1px solid #f1f5f9; }
|
||||||
|
.split-table tbody tr:hover { background: #f8fafd; }
|
||||||
|
.split-table td { padding: 12px; vertical-align: middle; }
|
||||||
|
.split-table td:last-child { text-align: right; }
|
||||||
|
.split-name { font-weight: 700; font-size: 13px; }
|
||||||
|
.split-dates { font-size: 11px; color: #7c8db0; font-weight: 600; margin-top: 1px; }
|
||||||
|
.mini-prog-wrap { width: 80px; height: 4px; background: #e4eaf5; border-radius: 99px; overflow: hidden; margin-top: 4px; }
|
||||||
|
.mini-prog-bar { height: 100%; border-radius: 99px; }
|
||||||
|
.split-pct-badge { display: inline-block; padding: 3px 10px; border-radius: 99px; font-size: 11px; font-weight: 700; }
|
||||||
|
.act-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
|
||||||
|
.act-chip { display: flex; align-items: center; gap: 6px; background: #f1f5f9; border-radius: 99px; padding: 5px 13px; font-size: 12px; font-weight: 700; }
|
||||||
|
.act-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||||
|
|
||||||
|
/* Opportunities tab */
|
||||||
|
.opp-summary-cards { display: grid; grid-template-columns: repeat(3,1fr); gap: 12px; margin-bottom: 20px; }
|
||||||
|
.opp-card { background: #f8fafd; border-radius: 12px; padding: 16px; border: 1px solid #e4eaf5; text-align: center; }
|
||||||
|
.opp-card-val { font-size: 22px; font-weight: 800; color: #0f172a; }
|
||||||
|
.opp-card-lbl { font-size: 11px; color: #7c8db0; font-weight: 700; margin-top: 4px; letter-spacing: .04em; }
|
||||||
|
|
||||||
|
@media(max-width:900px) { .list-col-head,.achieve-row { grid-template-columns: 220px 1fr 110px 110px 110px; } .col-lbl:nth-child(6),.col-lbl:nth-child(7),.achieve-row>*:nth-child(6),.achieve-row>*:nth-child(7) { display: none; } }
|
||||||
|
@media(max-width:640px) { .list-col-head { display: none; } .achieve-row { grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px; } .achieve-row>*:not(:nth-child(1)):not(:nth-child(7)) { display: none; } .section-head { flex-direction: column; align-items: flex-start; gap: 12px; } .modal-summary { grid-template-columns: repeat(2,1fr); } .opp-summary-cards { grid-template-columns: 1fr 1fr; } }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="dash-container">
|
<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> -->
|
|
||||||
|
|
||||||
|
<!-- ── Dashboard Header ── -->
|
||||||
|
<div class="dash-header">
|
||||||
|
<div>
|
||||||
|
<div class="page-title">Branch Dashboard</div>
|
||||||
|
<div class="page-sub" id="fySubLabel">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="fy-select-wrap">
|
||||||
|
<select id="financial_year" name="financial_year" onchange="onFinancialYearChange(this)">
|
||||||
|
<?php if(!empty($fin_years)): ?>
|
||||||
|
<?php foreach($fin_years as $year): ?>
|
||||||
|
<option value="<?= $year; ?>"><?= $year; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Stat Cards ── -->
|
||||||
<div class="stat-cards">
|
<div class="stat-cards">
|
||||||
<div class="card-hero">
|
<div class="card-hero">
|
||||||
|
<div class="stat-icon" style="background:#ede9fe;">👥</div>
|
||||||
<div class="stat-val"><?php echo $total_leads; ?></div>
|
<div class="stat-val"><?php echo $total_leads; ?></div>
|
||||||
<div class="stat-label">Total Leads</div>
|
<div class="stat-label">Total Leads</div>
|
||||||
<!-- <div class="stat-change text-success">↑ 12% this month</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-hero">
|
<div class="card-hero">
|
||||||
|
<div class="stat-icon" style="background:#fce7f3;">⚡</div>
|
||||||
<div class="stat-val"><?php echo $total_acts ?></div>
|
<div class="stat-val"><?php echo $total_acts ?></div>
|
||||||
<div class="stat-label">Total Activities</div>
|
<div class="stat-label">Total Activities</div>
|
||||||
<!-- <div class="stat-change text-success">↑ 5% this month</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-hero">
|
<div class="card-hero">
|
||||||
|
<div class="stat-icon" style="background:#fef3c7;">⏳</div>
|
||||||
<div class="stat-val"><?php echo $total_pending_acts ?></div>
|
<div class="stat-val"><?php echo $total_pending_acts ?></div>
|
||||||
<div class="stat-label">Pending Activities</div>
|
<div class="stat-label">Pending Activities</div>
|
||||||
<!-- <div class="stat-change text-success">↑ 15% this month</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-hero">
|
<div class="card-hero">
|
||||||
|
<div class="stat-icon" style="background:#d1fae5;">✅</div>
|
||||||
<div class="stat-val"><?php echo $total_completed_acts ?></div>
|
<div class="stat-val"><?php echo $total_completed_acts ?></div>
|
||||||
<div class="stat-label">Completed Activities</div>
|
<div class="stat-label">Completed Activities</div>
|
||||||
<!-- <div class="stat-change text-success">↑ 15% this month</div> -->
|
|
||||||
</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>
|
||||||
|
|
||||||
|
<!-- ══ TEAM ACHIEVEMENT SECTION ══ -->
|
||||||
|
<div class="section-head">
|
||||||
|
<div>
|
||||||
|
<div class="section-title">Team Achievement Overview</div>
|
||||||
|
<!-- <div class="section-sub">Click any row to view split-wise & opportunities achievement</div> -->
|
||||||
|
<div class="section-sub"></div>
|
||||||
|
</div>
|
||||||
|
<div class="branch-summary">
|
||||||
|
<div style="font-size:11px;color:#7c8db0;font-weight:700;">Branch Total</div>
|
||||||
|
<div class="branch-amt" id="branchTotalAmt">—</div>
|
||||||
|
<div class="branch-bar-wrap"><div class="branch-bar-fill" id="branchBarFill" style="width:0%;"></div></div>
|
||||||
|
<div class="branch-pct-lbl" id="branchPctLbl">—</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="achieve-list">
|
||||||
|
<div class="list-col-head">
|
||||||
|
<div class="col-lbl">Member</div>
|
||||||
|
<div class="col-lbl">Progress</div>
|
||||||
|
<div class="col-lbl right">Achieved</div>
|
||||||
|
<div class="col-lbl right">Target</div>
|
||||||
|
<div class="col-lbl right">Remaining</div>
|
||||||
|
<div class="col-lbl center">Status</div>
|
||||||
|
<div class="col-lbl center">Acts</div>
|
||||||
|
</div>
|
||||||
|
<div id="achieveList"></div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<!-- ── Main Grid ── -->
|
||||||
<div class="main-grid">
|
<div class="main-grid">
|
||||||
|
|
||||||
|
<!-- LEFT: Pending Activities -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<h3 style="font-size: 16px; font-weight: 700;">Pending Activities - All Team</h3>
|
<h3 style="font-size:15px; font-weight:700; margin:0;">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>
|
<span style="color:#8896aa; font-size:12px; font-weight:600;">
|
||||||
|
<?= count($pending_acts) . ' ' . (count($pending_acts) == 1 ? 'activity' : 'activities') ?>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="table-scroll-area">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Company</th>
|
<th>Company</th>
|
||||||
<th>Activity & Assigned</th>
|
<th>Activity & Assigned</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -129,15 +319,7 @@
|
|||||||
<?php if (!empty($pending_acts)): ?>
|
<?php if (!empty($pending_acts)): ?>
|
||||||
<?php foreach ($pending_acts as $a): ?>
|
<?php foreach ($pending_acts as $a): ?>
|
||||||
<?php
|
<?php
|
||||||
$activityIcons = [
|
$activityIcons = ['Call'=>'📞','Email'=>'✉️','Meeting'=>'📅','Visit'=>'🚗','Demo'=>'🖥️','Share Docs'=>'📄','To Do'=>'✓'];
|
||||||
'Call' => '📞',
|
|
||||||
'Email' => '✉️',
|
|
||||||
'Meeting' => '📅',
|
|
||||||
'Visit' => '🚗',
|
|
||||||
'Demo' => '🖥️',
|
|
||||||
'Share Docs' => '📄',
|
|
||||||
'To Do' => '✓'
|
|
||||||
];
|
|
||||||
$icon = $activityIcons[$a['activity_type']] ?? '📌';
|
$icon = $activityIcons[$a['activity_type']] ?? '📌';
|
||||||
$statusClass = strtolower(str_replace(' ', '-', $a['status']));
|
$statusClass = strtolower(str_replace(' ', '-', $a['status']));
|
||||||
$formattedscheduledDate = date('M d, Y, h:i A', strtotime($a['scheduled_date']));
|
$formattedscheduledDate = date('M d, Y, h:i A', strtotime($a['scheduled_date']));
|
||||||
@ -145,74 +327,56 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><strong><?= esc($a['company_name']) ?></strong></td>
|
<td><strong><?= esc($a['company_name']) ?></strong></td>
|
||||||
<td>
|
<td>
|
||||||
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">
|
<div style="color:#ff6b35; font-weight:700; font-size:11px;"><?= $icon ?> <?= strtoupper(esc($a['activity_type'])) ?></div>
|
||||||
<?= $icon ?> <?= strtoupper(esc($a['activity_type'])) ?>
|
<div style="font-size:12px; color:#8896aa;"><?= esc($a['assigned_to_name'] ?? 'ID: ' . $a['assigned_to']) ?></div>
|
||||||
</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>
|
||||||
<td style="color: #4a5568; font-weight: 500;"><?= $formattedscheduledDate ?></td>
|
<td style="color:#4a5568; font-weight:500; font-size:13px;"><?= $formattedscheduledDate ?></td>
|
||||||
<td><span class="status-pill status-<?= $statusClass ?>"><?= ucfirst(esc($a['status'])) ?></span></td>
|
<td><span class="status-pill status-<?= $statusClass ?>"><?= ucfirst(esc($a['status'])) ?></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<tr>
|
<tr><td colspan="4"><div class="empty-state"><div class="empty-icon">✓</div>No pending activities</div></td></tr>
|
||||||
<td colspan="4">
|
|
||||||
<div class="empty-state">
|
|
||||||
<div class="empty-icon">✓</div> No activities found
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<!-- RIGHT: Sales Team Performance + Activity Breakdown -->
|
||||||
<div class="card">
|
<div class="right-panel">
|
||||||
|
<div style="font-size:15px; font-weight:700; padding-bottom:10px; border-bottom:1px solid #f0f4fa; flex-shrink:0;">
|
||||||
<h3 style="font-size: 16px; font-weight: 700; margin-bottom: 15px;">Sales Team Performance</h3>
|
Sales Team Performance
|
||||||
<?php foreach ($team as $member) {
|
</div>
|
||||||
|
<div class="team-members-scroll">
|
||||||
|
<?php
|
||||||
|
if (!empty($team)):
|
||||||
|
$colors = ['#ff6b35','#667eea','#48bb78','#ed8936','#9f7aea'];
|
||||||
|
foreach ($team as $member):
|
||||||
$firstLetter = strtoupper(substr($member['first_name'], 0, 1));
|
$firstLetter = strtoupper(substr($member['first_name'], 0, 1));
|
||||||
$fullName = $member['first_name'] . ' ' . $member['last_name'];
|
$fullName = $member['first_name'] . ' ' . $member['last_name'];
|
||||||
$totalActs = $member['total_acts'];
|
$color = $colors[abs(crc32($member['first_name'])) % count($colors)];
|
||||||
$doneActs = $member['done_acts'];
|
?>
|
||||||
$role = $member['role']; // fix your DB first
|
<div class="team-member">
|
||||||
|
<div class="member-img" style="background:<?= $color ?>;"><?= $firstLetter ?></div>
|
||||||
// Random or consistent color based on name
|
<div style="flex:1;">
|
||||||
$colors = ['#ff6b35', '#667eea', '#48bb78', '#ed8936', '#9f7aea'];
|
<div style="font-weight:700; font-size:14px;"><?= esc($fullName) ?></div>
|
||||||
$colorIndex = abs(crc32($member['first_name'])) % count($colors);
|
<div style="font-size:11px; color:#8896aa;"><?= esc($member['role']) ?></div>
|
||||||
$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>
|
||||||
<div class='act-stats'>
|
<div class="act-stats">
|
||||||
<div class='total'>{$totalActs} acts</div>
|
<div class="total"><?= $member['total_acts'] ?> acts</div>
|
||||||
<div class='done'>{$doneActs} done</div>
|
<div class="done"><?= $member['done_acts'] ?> done</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; else: ?>
|
||||||
|
<div class="empty-state">No team data found</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>";
|
|
||||||
} ?>
|
|
||||||
|
|
||||||
|
<div class="breakdown-section">
|
||||||
<div class="card breakdown-card">
|
<h3 style="font-size:13px; font-weight:700; margin:0 0 12px 0; color:#1a202c;">Activity Breakdown</h3>
|
||||||
<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
|
<?php
|
||||||
$activityConfig = [
|
$activityConfig = [
|
||||||
'Call' => ['color' => '#ff6b35', 'icon' => '📞'],
|
'Call' => ['color'=>'#667eea','icon'=>'📞'],
|
||||||
'Email' => ['color'=>'#48bb78','icon'=>'✉️'],
|
'Email' => ['color'=>'#48bb78','icon'=>'✉️'],
|
||||||
'Meeting' => ['color'=>'#4299e1','icon'=>'📅'],
|
'Meeting' => ['color'=>'#4299e1','icon'=>'📅'],
|
||||||
'Visit' => ['color'=>'#ecc94b','icon'=>'🚗'],
|
'Visit' => ['color'=>'#ecc94b','icon'=>'🚗'],
|
||||||
@ -221,50 +385,39 @@
|
|||||||
'To Do' => ['color'=>'#718096','icon'=>'✓'],
|
'To Do' => ['color'=>'#718096','icon'=>'✓'],
|
||||||
];
|
];
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if (!empty($activity_breakdown)): ?>
|
<?php if (!empty($activity_breakdown)): ?>
|
||||||
<?php foreach ($activity_breakdown as $item): ?>
|
<?php foreach ($activity_breakdown as $item): ?>
|
||||||
<?php
|
<?php $cfg = $activityConfig[$item['activity_type']] ?? ['color'=>'#718096','icon'=>'📌']; ?>
|
||||||
$type = $item['activity_type'];
|
|
||||||
$color = $activityConfig[$type]['color'] ?? '#718096';
|
|
||||||
$icon = $activityConfig[$type]['icon'] ?? '📌';
|
|
||||||
?>
|
|
||||||
<div class="breakdown-item">
|
<div class="breakdown-item">
|
||||||
<span>
|
<span style="min-width:90px;"><?= $cfg['icon'] ?> <?= esc($item['activity_type']) ?></span>
|
||||||
<span style="background: <?= $color ?>;"></span>
|
<div class="breakdown-bar-wrap">
|
||||||
<?= $icon ?> <?= esc($type) ?>
|
<div class="breakdown-bar" style="width:<?= $item['percentage'] ?>%; background:<?= $cfg['color'] ?>;"></div>
|
||||||
</span>
|
</div>
|
||||||
<strong><?= $item['percentage'] ?>%</strong>
|
<strong style="min-width:34px; text-align:right; color:#1a202c;"><?= $item['percentage'] ?>%</strong>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="empty-state">
|
<div style="text-align:center; color:#aaa; font-size:13px;">No activities found</div>
|
||||||
<div class="empty-icon">✓</div> No activities found
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="grid-column: 1 / -1; width: 100%; box-sizing: border-box;">
|
</div><!-- end .main-grid -->
|
||||||
|
|
||||||
|
<!-- ── All Leads Overview ── -->
|
||||||
|
<div class="card" style="width:100%; box-sizing:border-box;">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<h3 style="font-size: 16px; font-weight: 700;">All Leads Overview </h3>
|
<h3 style="font-size:15px; font-weight:700; margin:0;">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>
|
</div>
|
||||||
|
<div class="leads-table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Company</th>
|
<th>Company</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Assigned To</th>
|
<th>Assigned To</th>
|
||||||
<th style="text-align: center !important;">Activities</th>
|
<th style="text-align:center;">Activities</th>
|
||||||
<th style="text-align: center !important;">Opportunities</th>
|
<th style="text-align:center;">Opportunities</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -280,15 +433,364 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<tr>
|
<tr><td colspan="5"><div class="empty-state"><div class="empty-icon">👤</div>No leads found</div></td></tr>
|
||||||
<td colspan="5">
|
|
||||||
<div class="empty-state">
|
|
||||||
<div class="empty-icon">👤</div> No leads found
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div><!-- end .dash-container -->
|
||||||
|
|
||||||
|
<!-- ══ MODAL ══ -->
|
||||||
|
<div class="modal-overlay" id="modalOverlay">
|
||||||
|
<div class="modal-box">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-ava" id="modalAva"></div>
|
||||||
|
<div>
|
||||||
|
<div class="modal-name" id="modalName"></div>
|
||||||
|
<div class="modal-fy-badge" id="modalFyBadge"></div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-summary" id="modalSummary"></div>
|
||||||
|
<div class="modal-body" id="modalBody"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ── Data injected from PHP ── */
|
||||||
|
const TEAM = <?php echo json_encode($team_achievement ?? []); ?>;
|
||||||
|
const OPP_DATA = <?php echo json_encode($opp_achievement ?? []); ?>;
|
||||||
|
/* OPP_DATA shape per userId:
|
||||||
|
{ total_policies, total_exp_amt, target_amt,
|
||||||
|
policies: [{ policy_no, issue_date, amount, created_at }] } */
|
||||||
|
|
||||||
|
/* ── JS Helpers ── */
|
||||||
|
const ACT_ICONS = {Call:'📞',Email:'✉️',Meeting:'📅',Visit:'🚗',Demo:'🖥️','Share Docs':'📄','To Do':'✓'};
|
||||||
|
const ACT_COLORS = {Call:'#4f46e5',Email:'#10b981',Meeting:'#06b6d4',Visit:'#f59e0b',Demo:'#ec4899','Share Docs':'#f97316','To Do':'#64748b'};
|
||||||
|
const fmt = v => '₹' + (v / 100000).toFixed(1) + 'L';
|
||||||
|
const fmtFull = v => '₹' + Number(v).toLocaleString('en-IN');
|
||||||
|
const pct = (a, t) => t > 0 ? Math.min(100, Math.round(a / t * 100)) : 0;
|
||||||
|
const badge = p => p >= 100 ? ['b-achieved','🏆 Achieved'] : p >= 75 ? ['b-ontrack','🎯 On Track'] : p >= 50 ? ['b-behind','⚡ Behind'] : ['b-atrisk','⚠️ At Risk'];
|
||||||
|
const progColor= p => p >= 100 ? 'linear-gradient(90deg,#10b981,#34d399)' : p >= 75 ? 'linear-gradient(90deg,#06b6d4,#67e8f9)' : p >= 50 ? 'linear-gradient(90deg,#f97316,#fbbf24)' : 'linear-gradient(90deg,#ef4444,#fca5a5)';
|
||||||
|
|
||||||
|
/* ── Date formatters ── */
|
||||||
|
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||||
|
|
||||||
|
function fmtIssueDate(str) {
|
||||||
|
// Input: "2026-03-10" or any date string → "Mar 10, 2026"
|
||||||
|
if (!str) return '—';
|
||||||
|
const d = new Date(str);
|
||||||
|
if (isNaN(d)) return str;
|
||||||
|
return MONTHS[d.getMonth()] + ' ' + String(d.getDate()).padStart(2,'0') + ', ' + d.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtCreatedAt(str) {
|
||||||
|
// Input: "2025-03-04 10:00:00" → "Mar 04, 2025, 10:00 AM"
|
||||||
|
if (!str) return '—';
|
||||||
|
const d = new Date(str.replace(' ', 'T'));
|
||||||
|
if (isNaN(d)) return str;
|
||||||
|
let h = d.getHours(), mi = d.getMinutes();
|
||||||
|
const ampm = h >= 12 ? 'PM' : 'AM';
|
||||||
|
h = h % 12 || 12;
|
||||||
|
return MONTHS[d.getMonth()] + ' ' + String(d.getDate()).padStart(2,'0') + ', ' + d.getFullYear()
|
||||||
|
+ ', ' + String(h).padStart(2,'0') + ':' + String(mi).padStart(2,'0') + ' ' + ampm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Financial Year helpers ── */
|
||||||
|
function getCurrentFY() {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
const month = today.getMonth();
|
||||||
|
const startYear = month >= 3 ? year : year - 1;
|
||||||
|
return startYear + '-' + String(startYear + 1);
|
||||||
|
}
|
||||||
|
function formatFYLabel(fy) {
|
||||||
|
if (!fy) return '';
|
||||||
|
const parts = fy.split('-');
|
||||||
|
if (parts.length === 2) return 'FY ' + parts[0] + '-' + parts[1].slice(-2);
|
||||||
|
return 'FY ' + fy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Init ── */
|
||||||
|
function initFY() {
|
||||||
|
const select = document.getElementById('financial_year');
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
let fy = params.get('fy');
|
||||||
|
if (!fy) {
|
||||||
|
fy = getCurrentFY();
|
||||||
|
let matched = false;
|
||||||
|
for (let opt of select.options) { if (opt.value === fy) { matched = true; break; } }
|
||||||
|
if (!matched && select.options.length > 0) fy = select.options[0].value;
|
||||||
|
}
|
||||||
|
select.value = fy;
|
||||||
|
updateFYLabel(fy);
|
||||||
|
renderBranchSummary();
|
||||||
|
renderList();
|
||||||
|
}
|
||||||
|
function updateFYLabel(fy) {
|
||||||
|
document.getElementById('fySubLabel').textContent = '' + formatFYLabel(fy);
|
||||||
|
}
|
||||||
|
function onFinancialYearChange(select) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('fy', select.value);
|
||||||
|
window.location.href = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Branch Summary ── */
|
||||||
|
function renderBranchSummary() {
|
||||||
|
if (!TEAM || TEAM.length === 0) return;
|
||||||
|
const totalAchieved = TEAM.reduce((s, m) => s + (parseFloat(m.achieved_amt) || 0), 0);
|
||||||
|
const totalTarget = TEAM.reduce((s, m) => s + (parseFloat(m.target_amt) || 0), 0);
|
||||||
|
const p = pct(totalAchieved, totalTarget);
|
||||||
|
document.getElementById('branchTotalAmt').innerHTML = fmt(totalAchieved) + ' <span style="font-size:13px;color:#7c8db0;font-weight:600;">/ ' + fmt(totalTarget) + '</span>';
|
||||||
|
document.getElementById('branchBarFill').style.width = p + '%';
|
||||||
|
document.getElementById('branchPctLbl').textContent = p + '% of branch target';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Achievement List ── */
|
||||||
|
function renderList() {
|
||||||
|
if (!TEAM || TEAM.length === 0) {
|
||||||
|
document.getElementById('achieveList').innerHTML = '<div class="empty-state"><div class="empty-icon">👤</div>No team achievement data</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sorted = [...TEAM].sort((a, b) => pct(b.achieved_amt, b.target_amt) - pct(a.achieved_amt, a.target_amt));
|
||||||
|
document.getElementById('achieveList').innerHTML = sorted.map((m, i) => {
|
||||||
|
const p = pct(m.achieved_amt, m.target_amt);
|
||||||
|
const rem = Math.max(0, m.target_amt - m.achieved_amt);
|
||||||
|
const [bc, bt] = badge(p);
|
||||||
|
const grad = m.grad || 'linear-gradient(135deg,#4f46e5,#818cf8)';
|
||||||
|
const color = m.color || '#4f46e5';
|
||||||
|
return '<div class="achieve-row" onclick="openModal(' + m.id + ')">'
|
||||||
|
+ '<div class="member-cell">'
|
||||||
|
+ '<div style="font-size:14px;font-weight:800;color:#cbd5e1;min-width:26px;">#' + (i+1) + '</div>'
|
||||||
|
+ '<div class="m-avatar" style="background:' + grad + ';">' + (m.first_name||'?')[0] + '</div>'
|
||||||
|
+ '<div><div class="m-name">' + m.first_name + ' ' + m.last_name + '</div></div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div style="padding-right:20px;">'
|
||||||
|
+ '<div class="prog-row"><span class="prog-pct" style="color:' + color + ';">' + p + '%</span><span class="prog-amts">' + fmt(m.achieved_amt) + ' / ' + fmt(m.target_amt) + '</span></div>'
|
||||||
|
+ '<div class="prog-bar-wrap"><div class="prog-bar" style="width:' + p + '%;background:' + grad + ';"></div></div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="num-cell"><div class="num-val">' + fmt(m.achieved_amt) + '</div></div>'
|
||||||
|
+ '<div class="num-cell"><div class="num-val" style="color:#7c8db0;">' + fmt(m.target_amt) + '</div></div>'
|
||||||
|
+ '<div class="num-cell"><div class="num-val" style="color:' + (rem > 0 ? '#f97316' : '#10b981') + ';">' + (rem > 0 ? fmt(rem) : '—') + '</div></div>'
|
||||||
|
+ '<div style="display:flex;justify-content:center;"><span class="badge ' + bc + '">' + bt + '</span></div>'
|
||||||
|
+ '<div class="num-cell center-col"><div class="num-val">' + m.done_acts + '<span style="color:#cbd5e1;font-size:12px;font-weight:600;">/' + m.total_acts + '</span></div><div class="num-lbl">DONE/TOTAL</div></div>'
|
||||||
|
+ '<span class="row-arrow">›</span>'
|
||||||
|
+ '</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Modal ── */
|
||||||
|
function openModal(id) {
|
||||||
|
const m = TEAM.find(x => x.id == id);
|
||||||
|
if (!m) return;
|
||||||
|
|
||||||
|
const p = pct(m.achieved_amt, m.target_amt);
|
||||||
|
const rem = Math.max(0, m.target_amt - m.achieved_amt);
|
||||||
|
const [bc, bt] = badge(p);
|
||||||
|
const grad = m.grad || 'linear-gradient(135deg,#4f46e5,#818cf8)';
|
||||||
|
|
||||||
|
const fySelect = document.getElementById('financial_year');
|
||||||
|
const currentFY = fySelect ? fySelect.value : getCurrentFY();
|
||||||
|
const fyLabel = formatFYLabel(currentFY);
|
||||||
|
|
||||||
|
document.getElementById('modalAva').style.background = grad;
|
||||||
|
document.getElementById('modalAva').textContent = (m.first_name || '?')[0];
|
||||||
|
document.getElementById('modalName').textContent = m.first_name + ' ' + m.last_name;
|
||||||
|
document.getElementById('modalFyBadge').textContent = fyLabel;
|
||||||
|
|
||||||
|
document.getElementById('modalSummary').innerHTML =
|
||||||
|
'<div class="ms-box"><div class="ms-num" style="color:#4f46e5;">' + fmt(m.achieved_amt) + '</div><div class="ms-lbl">ACHIEVED</div></div>'
|
||||||
|
+ '<div class="ms-box"><div class="ms-num">' + fmt(m.target_amt) + '</div><div class="ms-lbl">TARGET</div></div>'
|
||||||
|
+ '<div class="ms-box"><div class="ms-num" style="color:#f97316;">' + fmt(rem) + '</div><div class="ms-lbl">REMAINING</div></div>'
|
||||||
|
+ '<div class="ms-box"><div class="ms-num" style="color:' + (p>=75?'#10b981':p>=50?'#f97316':'#ef4444') + ';">' + p + '%</div><div class="ms-lbl">ACHIEVED %</div></div>';
|
||||||
|
|
||||||
|
/* ── Overall progress block (shared) ── */
|
||||||
|
const overallHtml =
|
||||||
|
'<div class="overall-prog">'
|
||||||
|
+ '<div class="op-label">OVERALL TARGET ACHIEVEMENT</div>'
|
||||||
|
+ '<div class="op-row">'
|
||||||
|
+ '<div><div class="op-achieved">' + fmt(m.achieved_amt) + '</div><div class="op-target">of ' + fmt(m.target_amt) + ' target</div></div>'
|
||||||
|
+ '<div class="op-pct">' + p + '%</div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="op-bar-wrap"><div class="op-bar-fill" style="width:' + p + '%;"></div></div>'
|
||||||
|
+ '<div class="op-foot"><span>Remaining: <strong style="color:#fbbf24;">' + fmt(rem) + '</strong></span>'
|
||||||
|
+ '<span class="badge ' + bc + '" style="background:rgba(255,255,255,.1);color:#e2e8f0;">' + bt + '</span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
|
||||||
|
/* ════════════════════════════
|
||||||
|
TAB 1 — Quarter-wise splits
|
||||||
|
════════════════════════════ */
|
||||||
|
const splits = m.splits || [];
|
||||||
|
const splitRows = splits.map(function(s) {
|
||||||
|
const sp = pct(s.achieved, s.target);
|
||||||
|
const srem = Math.max(0, s.target - s.achieved);
|
||||||
|
const [sbc, sbt] = badge(sp);
|
||||||
|
|
||||||
|
/* % cell: number on top, badge below (badge hidden when 0%) */
|
||||||
|
const pctColor = sp >= 75 ? '#10b981' : sp >= 50 ? '#f97316' : '#ef4444';
|
||||||
|
const pctCell = '<div style="font-weight:800;font-size:15px;color:' + pctColor + ';">' + sp + '%</div>'
|
||||||
|
+ (sp > 0 ? '<div style="margin-top:3px;"><span class="split-pct-badge ' + sbc + '" style="font-size:10px;padding:2px 8px;">' + sbt + '</span></div>' : '');
|
||||||
|
|
||||||
|
/* Activities cell: total bold on top, done in muted smaller text below */
|
||||||
|
const actsCell = '<div style="font-weight:800;font-size:15px;">' + s.acts + '</div>'
|
||||||
|
+ '<div style="font-size:11px;color:#94a3b8;font-weight:600;margin-top:2px;">' + s.done + ' done</div>';
|
||||||
|
|
||||||
|
return '<tr>'
|
||||||
|
+ '<td><div class="split-name">' + s.name + '</div><div class="split-dates">📅 ' + s.start + ' → ' + s.end + '</div></td>'
|
||||||
|
+ '<td><div style="font-weight:800;">' + fmt(s.achieved) + '</div><div class="mini-prog-wrap"><div class="mini-prog-bar" style="width:' + sp + '%;background:' + progColor(sp) + ';"></div></div></td>'
|
||||||
|
+ '<td style="font-weight:700;color:#7c8db0;">' + fmt(s.target) + '</td>'
|
||||||
|
+ '<td style="font-weight:700;color:#f97316;">' + (srem > 0 ? fmt(srem) : '—') + '</td>'
|
||||||
|
+ '<td>' + pctCell + '</td>'
|
||||||
|
+ '<td>' + actsCell + '</td>'
|
||||||
|
+ '<td style="font-weight:700;">' + s.leads + '</td>'
|
||||||
|
+ '</tr>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const actChips = Object.entries(m.activities || {}).map(function(entry) {
|
||||||
|
const k = entry[0], v = entry[1];
|
||||||
|
return '<div class="act-chip">'
|
||||||
|
+ '<div class="act-dot" style="background:' + (ACT_COLORS[k]||'#64748b') + ';"></div>'
|
||||||
|
+ (ACT_ICONS[k]||'📌') + ' ' + k + ': <strong>' + v + '</strong>'
|
||||||
|
+ '</div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const tab1Html = overallHtml
|
||||||
|
+ '<div class="split-section-title">📊 Quarter-wise Achievement Breakdown</div>'
|
||||||
|
+ '<div style="overflow-x:auto;margin-bottom:20px;">'
|
||||||
|
+ '<table class="split-table">'
|
||||||
|
+ '<thead><tr>'
|
||||||
|
+ '<th>Quarter</th><th>Achieved</th><th>Target</th><th>Remaining</th>'
|
||||||
|
+ '<th style="text-align:center;">Achievement %</th>'
|
||||||
|
+ '<th style="text-align:center;">Activities</th>'
|
||||||
|
+ '<th style="text-align:center;">Leads</th>'
|
||||||
|
+ '</tr></thead>'
|
||||||
|
+ '<tbody>' + (splitRows || '<tr><td colspan="7" style="text-align:center;color:#aaa;padding:20px;">No split data available</td></tr>') + '</tbody>'
|
||||||
|
+ '</table>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="split-section-title">⚡ Activity Breakdown</div>'
|
||||||
|
+ '<div class="act-chips">' + (actChips || '<span style="color:#aaa;font-size:13px;">No activity data</span>') + '</div>';
|
||||||
|
|
||||||
|
/* ════════════════════════════
|
||||||
|
TAB 2 — Opportunities
|
||||||
|
════════════════════════════ */
|
||||||
|
const opp = OPP_DATA[id] || null;
|
||||||
|
let tab2Html = '';
|
||||||
|
|
||||||
|
if (opp) {
|
||||||
|
const oppPct = pct(opp.total_exp_amt, opp.target_amt);
|
||||||
|
const [obc, obt] = badge(oppPct);
|
||||||
|
const oppPctColor = oppPct >= 75 ? '#10b981' : oppPct >= 50 ? '#f97316' : '#ef4444';
|
||||||
|
|
||||||
|
/* Summary cards */
|
||||||
|
const summaryCards =
|
||||||
|
'<div class="opp-summary-cards">'
|
||||||
|
+ '<div class="opp-card"><div class="opp-card-val" style="color:#4f46e5;">' + opp.total_policies + '</div><div class="opp-card-lbl">TOTAL POLICIES WON</div></div>'
|
||||||
|
+ '<div class="opp-card"><div class="opp-card-val" style="color:#10b981;">' + fmtFull(opp.total_exp_amt) + '</div><div class="opp-card-lbl">TOTAL EXP AMOUNT</div></div>'
|
||||||
|
+ '<div class="opp-card"><div class="opp-card-val" style="color:' + oppPctColor + ';">' + oppPct + '%</div><div class="opp-card-lbl">VS TARGET</div></div>'
|
||||||
|
+ '</div>';
|
||||||
|
|
||||||
|
/* Dark progress bar */
|
||||||
|
const oppProgress =
|
||||||
|
'<div style="background:linear-gradient(135deg,#0f172a,#1e293b);border-radius:14px;padding:18px 20px;color:#fff;margin-bottom:20px;">'
|
||||||
|
+ '<div class="op-label">OPPORTUNITIES ACHIEVEMENT</div>'
|
||||||
|
+ '<div class="op-row">'
|
||||||
|
+ '<div><div class="op-achieved">' + fmtFull(opp.total_exp_amt) + '</div><div class="op-target">of ' + fmtFull(opp.target_amt) + ' target</div></div>'
|
||||||
|
+ '<div class="op-pct">' + oppPct + '%</div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="op-bar-wrap"><div class="op-bar-fill" style="width:' + oppPct + '%;"></div></div>'
|
||||||
|
+ '<div class="op-foot"><span>' + opp.total_policies + ' policies won</span>'
|
||||||
|
+ '<span class="badge ' + obc + '" style="background:rgba(255,255,255,.1);color:#e2e8f0;">' + obt + '</span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
|
||||||
|
/* Policy rows — NO quarter grouping */
|
||||||
|
const policies = opp.policies || [];
|
||||||
|
const policyRows = policies.map(function(pol) {
|
||||||
|
return '<tr>'
|
||||||
|
+ '<td style="font-weight:700;color:#0f172a;">' + (pol.policy_no || '—') + '</td>'
|
||||||
|
+ '<td style="font-weight:600;color:#334155;">' + fmtIssueDate(pol.issue_date) + '</td>'
|
||||||
|
+ '<td style="font-weight:800;color:#0f172a;">' + fmtFull(pol.amount || 0) + '</td>'
|
||||||
|
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(pol.created_at) + '</td>'
|
||||||
|
+ '</tr>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
tab2Html = "";
|
||||||
|
// tab2Html = summaryCards
|
||||||
|
// + oppProgress
|
||||||
|
// + '<div class="split-section-title">📋 Policy Details</div>'
|
||||||
|
// + '<div style="overflow-x:auto;">'
|
||||||
|
// + '<table class="split-table">'
|
||||||
|
// + '<thead><tr>'
|
||||||
|
// + '<th>Policy No.</th>'
|
||||||
|
// + '<th>Issue Date</th>'
|
||||||
|
// + '<th>Amount</th>'
|
||||||
|
// + '<th>Created Date</th>'
|
||||||
|
// + '</tr></thead>'
|
||||||
|
// + '<tbody>' + (policyRows || '<tr><td colspan="4" style="text-align:center;color:#aaa;padding:24px;">No policies found</td></tr>') + '</tbody>'
|
||||||
|
// + '</table>'
|
||||||
|
// + '</div>';
|
||||||
|
|
||||||
|
/* ── Won Leads Table (Table 2) ── */
|
||||||
|
const wonLeads = opp.won_leads || [];
|
||||||
|
const wonRows = wonLeads.map(function(l) {
|
||||||
|
const typeBadgeClass = l.lead_type === 'EB'
|
||||||
|
? 'style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"'
|
||||||
|
: 'style="background:#f0fdf4;color:#16a34a;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"';
|
||||||
|
return '<tr>'
|
||||||
|
+ '<td style="font-weight:700;color:#0f172a;">' + (l.company || '—') + '</td>'
|
||||||
|
+ '<td><span ' + typeBadgeClass + '>' + (l.lead_type || '—') + '</span></td>'
|
||||||
|
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(l.created_at) + '</td>'
|
||||||
|
+ '</tr>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
tab2Html +=
|
||||||
|
'<div class="split-section-title" style="margin-top:24px;"> Opportunities </div>'
|
||||||
|
+ '<div style="overflow-x:auto;">'
|
||||||
|
+ '<table class="split-table">'
|
||||||
|
+ '<thead><tr>'
|
||||||
|
+ '<th>Company</th>'
|
||||||
|
+ '<th>Opportunity Type</th>'
|
||||||
|
+ '<th>Date</th>'
|
||||||
|
+ '</tr></thead>'
|
||||||
|
+ '<tbody>' + (wonRows || '<tr><td colspan="3" style="text-align:center;color:#aaa;padding:24px;">No leads found</td></tr>') + '</tbody>'
|
||||||
|
+ '</table>'
|
||||||
|
+ '</div>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
tab2Html = '<div class="empty-state"><div class="empty-icon">📋</div>No opportunities data available</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Inject tabs ── */
|
||||||
|
document.getElementById('modalBody').innerHTML =
|
||||||
|
'<div class="modal-tabs">'
|
||||||
|
+ '<button class="modal-tab active" onclick="switchTab(0,this)">📊 ' + fyLabel + ' Split-wise Achievement</button>'
|
||||||
|
+ '<button class="modal-tab" onclick="switchTab(1,this)">🏆 Opportunities Achievement</button>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="modal-tab-panel active" id="tabPanel0">' + tab1Html + '</div>'
|
||||||
|
+ '<div class="modal-tab-panel" id="tabPanel1">' + tab2Html + '</div>';
|
||||||
|
|
||||||
|
document.getElementById('modalOverlay').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(idx, btn) {
|
||||||
|
document.querySelectorAll('.modal-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||||
|
document.querySelectorAll('.modal-tab-panel').forEach(function(p) { p.classList.remove('active'); });
|
||||||
|
btn.classList.add('active');
|
||||||
|
document.getElementById('tabPanel' + idx).classList.add('active');
|
||||||
|
}
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('modalOverlay').classList.remove('open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('modalOverlay').addEventListener('click', function(e) { if (e.target === e.currentTarget) closeModal(); });
|
||||||
|
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); });
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initFY);
|
||||||
|
</script>
|
||||||
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> -->
|
<p style="color:#666; font-size:13px; margin-top: 4px;">Welcome back, <?= $user_name ?></p> -->
|
||||||
</div>
|
</div>
|
||||||
<div style="position: relative;">
|
<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 if(!empty($fin_years)): ?>
|
||||||
<?php foreach($fin_years as $year): ?>
|
<?php foreach($fin_years as $year): ?>
|
||||||
<option value="<?= $year; ?>"><?= $year; ?></option>
|
<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>
|
<button class="badge-done" onclick="openComp(<?= $u['activity_id'] ?>,<?= $u['lead_id'] ?>)">✓ Mark as completed</button>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?> -->
|
<?php endforeach; ?> -->
|
||||||
|
<?php if (!empty($upcoming)) : ?>
|
||||||
|
|
||||||
|
|
||||||
<?php foreach($upcoming as $u):
|
<?php foreach($upcoming as $u):
|
||||||
$activityIcons = [ 'Call' => '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share Docs' => '📄', 'To Do' => '✓' ];
|
$activityIcons = [ 'Call' => '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share Docs' => '📄', 'To Do' => '✓' ];
|
||||||
$icon = $activityIcons[$u['activity_type']] ?? '📌';
|
$icon = $activityIcons[$u['activity_type']] ?? '📌';
|
||||||
@ -160,11 +163,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
<?php else : ?>
|
||||||
|
|
||||||
|
<div class="text-center text-muted py-3">
|
||||||
|
No Upcoming Activities for this Financial Year.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="list-card">
|
<div class="list-card">
|
||||||
<h3 style="font-size:16px; font-weight: 700; margin-bottom:20px;">My Recent Leads</h3>
|
<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): ?>
|
<?php foreach($recent_leads as $rl): ?>
|
||||||
<!-- <div class="list-item">
|
<!-- <div class="list-item">
|
||||||
<div style="display:flex; gap:12px; align-items:center;">
|
<div style="display:flex; gap:12px; align-items:center;">
|
||||||
@ -206,6 +217,13 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -307,12 +307,12 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const users = <?= json_encode($users ?? []) ?>;
|
const sales_manager = <?= json_encode($sales_manager ?? []) ?>;
|
||||||
|
|
||||||
/* ──────────────────────────────────────────
|
/* ──────────────────────────────────────────
|
||||||
DATA
|
DATA
|
||||||
────────────────────────────────────────── */
|
────────────────────────────────────────── */
|
||||||
const teamMembers = users.map(user => ({
|
const teamMembers = sales_manager.map(user => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.first_name + ' ' + user.last_name
|
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 { 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-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 */
|
||||||
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
||||||
@ -24,6 +26,11 @@
|
|||||||
.status-potential { background: #fff3e0; color: #f57c00; }
|
.status-potential { background: #fff3e0; color: #f57c00; }
|
||||||
.status-prospects { background: #e8f5e9; color: #388e3c; }
|
.status-prospects { background: #e8f5e9; color: #388e3c; }
|
||||||
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
.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 */
|
/* 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; }
|
.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-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;}
|
||||||
.opportunity-detail-item { font-size: 13px; color: #666;}
|
.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; }
|
.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 */
|
/* 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; }
|
.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;
|
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>
|
</style>
|
||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
@ -137,7 +160,7 @@
|
|||||||
<!-- RIGHT SIDE -->
|
<!-- RIGHT SIDE -->
|
||||||
<div class="lead-actions">
|
<div class="lead-actions">
|
||||||
<input type="text" class="search-input" id="mainSearch"
|
<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')">
|
<button class="btn-primary" onclick="openModal('addLeadModal')">
|
||||||
+ Add Lead
|
+ Add Lead
|
||||||
</button>
|
</button>
|
||||||
@ -166,7 +189,7 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">Add Lead</h4>
|
<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>
|
</div>
|
||||||
<form class="parsley-examples" id="addLeadForm" enctype="multipart/form-data">
|
<form class="parsley-examples" id="addLeadForm" enctype="multipart/form-data">
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
@ -203,8 +226,8 @@
|
|||||||
<label class="form-label">Assign To<span class="text-danger">*</span></label>
|
<label class="form-label">Assign To<span class="text-danger">*</span></label>
|
||||||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||||||
<option value="">Select User</option>
|
<option value="">Select User</option>
|
||||||
<?php foreach($users as $user): ?>
|
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
|
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -228,7 +251,7 @@
|
|||||||
<h2 id="det_company">Lead Detail</h2>
|
<h2 id="det_company">Lead Detail</h2>
|
||||||
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -270,7 +293,7 @@
|
|||||||
<div class="modal-content" style="max-width: 600px;">
|
<div class="modal-content" style="max-width: 600px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Add Activity</h2>
|
<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>
|
</div>
|
||||||
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
|
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
@ -305,12 +328,20 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Assigned To</label>
|
<label class="form-label">Assigned To</label>
|
||||||
<select id="act_owner" class="search-input searchable" style="width:100%">
|
<select id="act_owner" class="search-input searchable" style="width:100%">
|
||||||
<?php foreach($users as $user): ?>
|
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="form-group text-right mb-0">
|
<div class="form-group text-right mb-0">
|
||||||
@ -326,7 +357,7 @@
|
|||||||
<div class="modal-content" style="max-width: 675px;">
|
<div class="modal-content" style="max-width: 675px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>Complete Activity</h3>
|
<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>
|
</div>
|
||||||
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
|
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
@ -374,12 +405,20 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||||||
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
|
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
|
||||||
<?php foreach($users as $user): ?>
|
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@ -397,7 +436,7 @@
|
|||||||
<div class="modal-content" style="max-width: 600px;">
|
<div class="modal-content" style="max-width: 600px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Select Opportunity Type</h2>
|
<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>
|
</div>
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
<div class="modal-body p-4">
|
<div class="modal-body p-4">
|
||||||
@ -429,61 +468,12 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</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" id="editLeadModal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">Edit Lead</h4>
|
<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>
|
</div>
|
||||||
<form class="parsley-examples" id="editLeadForm" enctype="multipart/form-data">
|
<form class="parsley-examples" id="editLeadForm" enctype="multipart/form-data">
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
@ -525,44 +515,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mb-1" style="padding-left: 25px;">
|
<style>
|
||||||
<label class="form-label">Contact Persons</label>
|
.g-0-5 { --bs-gutter-x: 0.3rem; }
|
||||||
<div id="contactPersonsContainer">
|
.g-0-5 > * { padding-right: calc(var(--bs-gutter-x) * .5); padding-left: calc(var(--bs-gutter-x) * .5); }
|
||||||
<div id="savedContactsContainer"> <span id="NoData"> <center><i> No contact persons added yet </i> </center> </span> </div>
|
|
||||||
|
.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-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 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>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mb-1 ml-1 ">
|
<div class="col-md-2 col-6">
|
||||||
|
<button type="button" id="btnSaveContact" class="btn btn-sm btn-info text-white" style="width:85%">
|
||||||
<div class="row g-2 align-items-center">
|
✔ Save
|
||||||
|
|
||||||
<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,'')">
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<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
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
@ -580,8 +599,8 @@
|
|||||||
<label class="form-label">Assign To <span class="text-danger">*</span> </label>
|
<label class="form-label">Assign To <span class="text-danger">*</span> </label>
|
||||||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||||||
<option value="">Select User</option>
|
<option value="">Select User</option>
|
||||||
<?php foreach($users as $user): ?>
|
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
|
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -599,16 +618,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
resetFlatpicker();
|
resetFlatpicker();
|
||||||
$('.searchable').each(function() {
|
|
||||||
let parentModal = $(this).closest('.modal');
|
// Single selects
|
||||||
$(this).select2({
|
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||||||
|
let parentModal = el.closest('.modal');
|
||||||
|
$(el).select2({
|
||||||
placeholder: "Select..",
|
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(){
|
function resetFlatpicker(){
|
||||||
@ -626,7 +661,7 @@ function resetFlatpicker(){
|
|||||||
allowInput: false,
|
allowInput: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const salesManagerIds = <?= json_encode($sales_manager_ids ?? []) ?>;
|
const salesManagerHeadIds = <?= json_encode($sales_manager_with_head_ids ?? []) ?>;
|
||||||
const API = '<?= base_url('sales') ?>';
|
const API = '<?= base_url('sales') ?>';
|
||||||
let filter = 'all';
|
let filter = 'all';
|
||||||
let lead_id = null;
|
let lead_id = null;
|
||||||
@ -648,7 +683,12 @@ function closeModal(id) {
|
|||||||
const form = modal.querySelector('form'); // Find the form inside this specific modal
|
const form = modal.querySelector('form'); // Find the form inside this specific modal
|
||||||
if (form) {
|
if (form) {
|
||||||
form.reset(); // Resets standard inputs (text, email, select)
|
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
|
$(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'
|
switchTab('activity'); // Reset the tab back to 'activity'
|
||||||
document.getElementById('btn_add_opportunity').style.display = 'none';
|
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>`;
|
</div>`;
|
||||||
|
|
||||||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
// 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.");
|
console.log("No Sales Manager IDs found. Skipping API call.");
|
||||||
grid.innerHTML = emptyStateHTML;
|
grid.innerHTML = emptyStateHTML;
|
||||||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||||||
@ -759,9 +808,9 @@ async function fetchLeads(isLoadMore = false) {
|
|||||||
|
|
||||||
// 5. Build URL with dynamic offset
|
// 5. Build URL with dynamic offset
|
||||||
let url = `${API}/leads?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
|
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!
|
// url += `&assigned_to=${salesManagerHeadIds.join(',')}`; // We already proved it exists above!
|
||||||
if (typeof salesManagerIds !== 'undefined' && salesManagerIds.length > 0) {
|
if (typeof salesManagerHeadIds !== 'undefined' && salesManagerHeadIds.length > 0) {
|
||||||
url += `&assigned_to=${salesManagerIds.join(',')}`;
|
url += `&assigned_to=${salesManagerHeadIds.join(',')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Fetching API:", url);
|
console.log("Fetching API:", url);
|
||||||
@ -774,6 +823,17 @@ async function fetchLeads(isLoadMore = false) {
|
|||||||
const totalrecords = json.total || 0;
|
const totalrecords = json.total || 0;
|
||||||
console.log("API Response:", leadsData);
|
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
|
const html = leadsData
|
||||||
.sort((a, b) => Number(b.lead_id) - Number(a.lead_id))
|
.sort((a, b) => Number(b.lead_id) - Number(a.lead_id))
|
||||||
.map(l => {
|
.map(l => {
|
||||||
@ -801,7 +861,7 @@ async function fetchLeads(isLoadMore = false) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a class="text-body dropdown-toggle" href="#" data-toggle="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>
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
<a class="dropdown-item" href="javascript:void(0);" onclick="viewDetail(${l.lead_id})"> 👁 View</a>
|
<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) {
|
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 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', {
|
let formattedDate = new Date(o.created_at).toLocaleString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@ -924,11 +995,21 @@ function renderCard(opps) {
|
|||||||
<strong>Created At:</strong> ${formattedDate}
|
<strong>Created At:</strong> ${formattedDate}
|
||||||
</div>
|
</div>
|
||||||
<div class="opportunity-detail-item">
|
<div class="opportunity-detail-item">
|
||||||
<strong>Status:</strong> ${o.status}
|
<strong>Status:</strong> <span class="${statusClass}">${opp_status_value}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="opportunity-footer">
|
<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>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -936,15 +1017,7 @@ function renderCard(opps) {
|
|||||||
}
|
}
|
||||||
function renderTimeline(acts) {
|
function renderTimeline(acts) {
|
||||||
const cont = document.getElementById('timelineContainer');
|
const cont = document.getElementById('timelineContainer');
|
||||||
const activityIcons = {
|
const activityIcons = { "Call": "📞", "Email": "✉️", "Meeting": "📅", "Visit": "🚗", "Demo": "🖥️", "Share Docs": "📄", "To Do": "✓"};
|
||||||
"Call": "📞",
|
|
||||||
"Email": "✉️",
|
|
||||||
"Meeting": "📅",
|
|
||||||
"Visit": "🚗",
|
|
||||||
"Demo": "🖥️",
|
|
||||||
"Share Docs": "📄",
|
|
||||||
"To Do": "✓"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (acts.length === 0) {
|
if (acts.length === 0) {
|
||||||
cont.classList.add('no-line');
|
cont.classList.add('no-line');
|
||||||
@ -972,26 +1045,38 @@ function renderTimeline(acts) {
|
|||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
|
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
|
|
||||||
<b>${icon} ${formattedType}</b>
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:2px;">
|
||||||
<span style="font-size:11px; color:#999"> 👤 ${a.assigned_to_name} | 🗓 ${formattedDate}</span>
|
<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>
|
||||||
<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}
|
${a.notes}
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:13px; color:#444;"></div>
|
|
||||||
${a.status === 'pending' ?
|
${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>` :
|
`<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:8px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
`<div style="font-size:12px; color:#388e3c; margin-top:4px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>`;
|
||||||
`;
|
|
||||||
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</div>`;
|
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</div>`;
|
||||||
}
|
}
|
||||||
function openActivityModal() {
|
function openActivityModal() {
|
||||||
document.getElementById('act_owner').value = global_lead_assigned_to;
|
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;
|
document.getElementById('act_lead_id').value = lead_id;
|
||||||
openModal('activityModal');
|
openModal('activityModal');
|
||||||
}
|
}
|
||||||
@ -1004,7 +1089,8 @@ function openComp(id, assigned_to, lead_id) {
|
|||||||
document.getElementById('comp_id').value = id;
|
document.getElementById('comp_id').value = id;
|
||||||
document.getElementById('lead_id').value = lead_id;
|
document.getElementById('lead_id').value = lead_id;
|
||||||
document.getElementById('f_assigned_to').value = assigned_to;
|
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')
|
document.getElementById('f_typeButtons').querySelectorAll('.f_activity_type')
|
||||||
.forEach(btn => btn.classList.remove('active'));
|
.forEach(btn => btn.classList.remove('active'));
|
||||||
selectedFollowUpActivityType = '';
|
selectedFollowUpActivityType = '';
|
||||||
@ -1250,11 +1336,14 @@ document.getElementById('editLeadForm').onsubmit = async (e) => {
|
|||||||
|
|
||||||
if(res.ok) {
|
if(res.ok) {
|
||||||
toastr.success('Leads Updated Successfully');
|
toastr.success('Leads Updated Successfully');
|
||||||
|
e.target.reset();
|
||||||
closeModal('editLeadModal');
|
closeModal('editLeadModal');
|
||||||
fetchLeads();
|
fetchLeads();
|
||||||
e.target.reset();
|
// CLEANUP: Reset the contact UI for the next time it opens
|
||||||
$('#savedContactsContainer').empty();
|
$('#contactPersonsList').find('.contact-card').remove();
|
||||||
$('#NoData').show();
|
$('#noContactPersonsData').show();
|
||||||
|
document.getElementById('editing_contact_id').value = '';
|
||||||
|
document.getElementById('btnSaveContact').innerHTML = "✔ Save";
|
||||||
} else {
|
} else {
|
||||||
let err = await res.json();
|
let err = await res.json();
|
||||||
if (res.status === 400) {
|
if (res.status === 400) {
|
||||||
@ -1346,7 +1435,9 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
|||||||
notes: actNotes, // Reused the trimmed variable from above
|
notes: actNotes, // Reused the trimmed variable from above
|
||||||
scheduled_date: dbFormat,
|
scheduled_date: dbFormat,
|
||||||
assigned_to: actOwner, // Reused the trimmed variable from above
|
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`, {
|
const res = await fetch(`${API}/activities`, {
|
||||||
@ -1483,7 +1574,8 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
|||||||
notes: document.getElementById('f_notes').value,
|
notes: document.getElementById('f_notes').value,
|
||||||
scheduled_date: dbFormat,
|
scheduled_date: dbFormat,
|
||||||
assigned_to: document.getElementById('f_assigned_to').value,
|
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`, {
|
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) {
|
async function openEditLeadModal(id) {
|
||||||
// 1. Reset UI State
|
// 1. Reset UI State
|
||||||
document.getElementById('hidden_lead_id').value = id;
|
document.getElementById('hidden_lead_id').value = id;
|
||||||
const container = $('#savedContactsContainer');
|
const container = $('#contactPersonsList');
|
||||||
// Remove only previous contact cards, keep the NoData span for now
|
// Remove only previous contact cards, keep the NoData span for now
|
||||||
container.find('.contact-card').remove();
|
container.find('.contact-card').remove();
|
||||||
|
|
||||||
@ -1757,35 +1714,49 @@ async function openEditLeadModal(id) {
|
|||||||
form.querySelector('[name="address"]').value = lead.address || '';
|
form.querySelector('[name="address"]').value = lead.address || '';
|
||||||
form.querySelector('[name="website"]').value = lead.website || '';
|
form.querySelector('[name="website"]').value = lead.website || '';
|
||||||
form.querySelector('[name="gst_number"]').value = lead.gst_number || '';
|
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"]').value = lead.assigned_to || '';
|
||||||
$(form.querySelector('[name="assigned_to"]')).trigger('change');
|
$(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)
|
// 4. Handle Contact Persons (Looping through the nested array)
|
||||||
const contacts = lead.contact_persons; // Array from your JSON
|
const contacts = lead.contact_persons; // Array from your JSON
|
||||||
|
|
||||||
if (contacts && contacts.length > 0) {
|
if (contacts && contacts.length > 0) {
|
||||||
$('#NoData').hide();
|
$('#noContactPersonsData').hide();
|
||||||
|
|
||||||
contacts.forEach(contact => {
|
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 = `
|
let contactHtml = `
|
||||||
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-3"
|
<div class="contact-card d-flex align-items-center mb-2 p-2 px-3"
|
||||||
style="background: #f8f8f8; border-radius: 8px;"
|
style="background: ${isPrimary ? '#fff9c4' : '#f8f8f8'}; border: 1px solid ${isPrimary ? '#fbc02d' : '#eee'}; border-radius: 8px; gap: 12px;">
|
||||||
data-id="${contact.contact_id}">
|
|
||||||
<div>
|
<div style="flex: 2; min-width: 0;">
|
||||||
<div class="fw-bold">${contact.name}</div>
|
<div class="fw-bold text-truncate" style="font-size:14px;">${contact.name} ${primaryBadge}</div>
|
||||||
<div class="text-muted">${contact.mobile}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
|
||||||
class="btn btn-danger btn-sm btnRemoveContact"
|
<div style="flex: 2; min-width: 0; color:#666; font-size:13px;">${contact.mobile}</div>
|
||||||
data-id="${contact.contact_id}">
|
<div style="flex: 2; min-width: 0; color:#666; font-size:13px;"><div class="text-truncate">${contact.designation}</div></div>
|
||||||
Remove
|
|
||||||
</button>
|
<div class="d-flex" style="gap:5px;">
|
||||||
</div>`;
|
<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);
|
container.append(contactHtml);
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
$('#NoData').show();
|
else {
|
||||||
|
$('#noContactPersonsData').show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Open the Modal
|
// 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) {
|
function convertDBFormatted(input) {
|
||||||
|
|
||||||
if (!input) return null;
|
if (!input) return null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user