FIX_SalesTracker2
This commit is contained in:
parent
a863e37623
commit
57ff91a9c8
@ -30,6 +30,10 @@ class Acl
|
||||
'#^/util/download_log#' => ['roles' => [ADMIN_ROLE_ID]],
|
||||
'#^/metaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
|
||||
'#^/metaTpaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
|
||||
'#^/sales/dashboard#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
|
||||
'#^/sales#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
|
||||
|
||||
|
||||
|
||||
// ===================== PUBLIC DOWNLOADS / FORMS =====================
|
||||
'#^/download-#' => ['public' => true],
|
||||
|
||||
@ -912,7 +912,7 @@ $routes->group('sales', function($routes) {
|
||||
|
||||
$routes->get('/', 'SalesController::index');
|
||||
|
||||
$routes->get('activities', 'SalesController::activities');
|
||||
$routes->get('loadactivities', 'SalesController::loadactivities');
|
||||
|
||||
$routes->get('page/(:segment)', 'SalesController::noPage/$1');
|
||||
|
||||
@ -996,6 +996,7 @@ $routes->group('sales', function($routes) {
|
||||
|
||||
|
||||
//Dashboard
|
||||
$routes->get('dashboard', 'SalesController::dashboard');
|
||||
$routes->get('branchLevelDashboard', 'SalesController::branchLevelDashboard');
|
||||
$routes->get('salesManagerLevelDashboard', 'SalesController::salesManagerLevelDashboard');
|
||||
});
|
||||
|
||||
@ -35,12 +35,11 @@ class SalesController extends BaseController
|
||||
$data = $this->getSalesStaffData();
|
||||
|
||||
$data['tab_name'] = 'Leads';
|
||||
$data['page_name'] = 'Leads';
|
||||
|
||||
$data['page_name'] = 'Leads';
|
||||
return $this->loadLayout('sales/tracker_view', $data);
|
||||
}
|
||||
|
||||
public function activities(){
|
||||
public function loadactivities(){
|
||||
$data = $this->getSalesStaffData();
|
||||
|
||||
$data['tab_name'] = 'Activities';
|
||||
@ -49,6 +48,7 @@ class SalesController extends BaseController
|
||||
$data['leads'] = $this->leadModel->select('lead_id, company_name')
|
||||
->orderBy('lead_id', 'DESC')
|
||||
->findAll();
|
||||
|
||||
|
||||
return $this->loadLayout('sales/activity_view', $data);
|
||||
}
|
||||
@ -59,42 +59,36 @@ class SalesController extends BaseController
|
||||
private function getSalesStaffData(): array
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$logged_user_id = get_session_userid();
|
||||
$logged_user_id = get_session_userid();
|
||||
$role = get_role_id();
|
||||
$team_id = user_team();
|
||||
|
||||
$data = [
|
||||
'users' => [],
|
||||
'sales_manager_ids' => []
|
||||
];
|
||||
|
||||
// Get the Branch ID, Role, Team, and Name of the logged-in user
|
||||
$row = $db->table('user_profiles up')
|
||||
->select('up.id, up.first_name, up.last_name, up.nhance_branch_id, up.role, ut.team_id')
|
||||
->join('user_teams ut', 'ut.user_id = up.id', 'left')
|
||||
->where('up.is_active', 1)
|
||||
->where('up.id', $logged_user_id)
|
||||
->get()
|
||||
->getRow();
|
||||
|
||||
$row = $db->table('user_profiles')->select('*')
|
||||
->where('is_active', 1)->where('id', $logged_user_id)
|
||||
->get()->getRow();
|
||||
|
||||
$nhance_branch_id = $row ? $row->nhance_branch_id : null;
|
||||
$role = $row ? $row->role : null;
|
||||
$team_id = $row ? $row->team_id : null;
|
||||
|
||||
|
||||
// Is the logged-in user a Sales Manager? (Role 4, Team 5)
|
||||
if ($role == 4 && $team_id == 5) {
|
||||
|
||||
if ($role == 4 && in_array(5, $team_id)) {
|
||||
$data['sales_manager_ids'] = [$logged_user_id];
|
||||
|
||||
$data['users'] = [
|
||||
[
|
||||
'id' => $row->id,
|
||||
'sales_manager' => trim($row->first_name . ' ' . $row->last_name),
|
||||
'first_name' => $row->first_name,
|
||||
'nhance_branch_id' => $nhance_branch_id
|
||||
]
|
||||
];
|
||||
|
||||
}
|
||||
// Otherwise fetch ALL sales managers in this branch
|
||||
elseif ($nhance_branch_id) {
|
||||
elseif (in_array($role,[1,2,3,4,5])) {
|
||||
|
||||
$data['users'] = $db->table('user_profiles up')
|
||||
->select('up.id, up.first_name, up.last_name, up.nhance_branch_id')
|
||||
@ -779,11 +773,59 @@ class SalesController extends BaseController
|
||||
|
||||
|
||||
// ==================== Dashboard ====================
|
||||
public function dashboard(){
|
||||
|
||||
public function branchLevelDashboard()
|
||||
$logged_user_id = get_session_userid();
|
||||
$role = get_role_id();
|
||||
$team_id = user_team();
|
||||
$payload = $this->request->getGet();
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$row = $db->table('user_profiles')
|
||||
->select('*')
|
||||
->where('is_active', 1)
|
||||
->where('id', $logged_user_id)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$nhance_branch_id = $row ? $row['nhance_branch_id'] : null;
|
||||
|
||||
// dd($logged_user_id, $nhance_branch_id, $role, $team_id );
|
||||
|
||||
if (in_array($role,[5])) {
|
||||
|
||||
$sales_manager_ids = array_column(
|
||||
$db->table('user_profiles up')
|
||||
->select('up.id')
|
||||
->join('user_teams ut', 'ut.user_id = up.id')
|
||||
->where([
|
||||
'up.is_active' => 1,
|
||||
'ut.is_active' => 1,
|
||||
'up.role' => 4,
|
||||
'ut.team_id' => 5,
|
||||
'up.nhance_branch_id' => $nhance_branch_id
|
||||
])
|
||||
->get()
|
||||
->getResultArray(),
|
||||
'id'
|
||||
);
|
||||
$this->branchLevelDashboard($nhance_branch_id,$sales_manager_ids);
|
||||
|
||||
}
|
||||
elseif ($role == 4 && in_array(5, $team_id)) {
|
||||
$sales_manager_ids = [$logged_user_id];
|
||||
$this->salesManagerLevelDashboard($logged_user_id,$sales_manager_ids, $payload);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function branchLevelDashboard($branchId,$sales_manager_ids)
|
||||
{
|
||||
$data['tab_name'] = 'Dashboard';
|
||||
$data['page_name'] = 'Dashboard';
|
||||
// Hardcoded branch ID as requested
|
||||
$branchId = 1;
|
||||
// $branchId = 1;
|
||||
|
||||
try {
|
||||
// 1. Lead Statistics
|
||||
@ -798,11 +840,24 @@ class SalesController extends BaseController
|
||||
|
||||
// 3. Team Performance (Aggregating activity counts per user)
|
||||
$db = \Config\Database::connect();
|
||||
// Final safe check
|
||||
if (empty($sales_manager_ids)) {
|
||||
// No valid IDs — skip queries or return empty
|
||||
$teamPerformance = [];
|
||||
$recentActivities = [];
|
||||
$leadsOverview = [];
|
||||
} else {
|
||||
|
||||
if (empty($sales_manager_ids) || !is_array($sales_manager_ids)) {
|
||||
$sales_manager_ids = array_filter((array) $sales_manager_ids); // removes null, "", 0
|
||||
}
|
||||
|
||||
$teamPerformance = $db->table('user_profiles as u')
|
||||
->select('u.first_name, u.last_name, u.profile as role,
|
||||
(SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id) as total_acts,
|
||||
(SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id AND status = "completed") as done_acts')
|
||||
->where('u.nhance_branch_id', $branchId)
|
||||
->whereIn('u.id', $sales_manager_ids)
|
||||
->where('u.is_active', 1)
|
||||
->get()->getResultArray();
|
||||
|
||||
@ -812,11 +867,48 @@ class SalesController extends BaseController
|
||||
->orderBy('sales_activities.scheduled_date', 'DESC')
|
||||
->limit(6)
|
||||
->findAll();
|
||||
|
||||
$sales_manager_ids = array_values(array_map('intval', $sales_manager_ids));
|
||||
$recentActivities = $db->table('sales_activities sa')
|
||||
->select('sa.activity_id,sa.lead_id,sa.activity_type,sa.scheduled_date,sa.status,sa.assigned_to,sal.company_name,up.first_name AS assigned_to_name')
|
||||
->join('(SELECT lead_id, MAX(company_name) AS company_name FROM sales_actual_leads GROUP BY lead_id) sal','sal.lead_id = sa.lead_id','left')
|
||||
->join('user_profiles up', 'up.id = sa.assigned_to', 'left')
|
||||
->orderBy('sa.scheduled_date', 'DESC')
|
||||
->limit(6)
|
||||
->whereIn('sa.assigned_to', $sales_manager_ids)
|
||||
->get()->getResultArray();
|
||||
|
||||
// 5. All Leads Overview
|
||||
$leadsOverview = $db->table('sales_actual_leads sal')
|
||||
->select('sal.lead_id,sal.company_name,sal.status,up.first_name AS assigned_to,
|
||||
COUNT(DISTINCT sa.activity_id) AS activities,
|
||||
COUNT(DISTINCT l.id) AS opportunities
|
||||
')
|
||||
->join('user_profiles up', 'up.id = sal.assigned_to', 'left')
|
||||
->join('sales_activities sa', 'sa.lead_id = sal.lead_id', 'left')
|
||||
->join('leads l', 'l.actual_lead_id = sal.lead_id', 'left')
|
||||
->groupBy('sal.lead_id, sal.company_name, sal.status, up.first_name')
|
||||
->having('COUNT(DISTINCT sa.activity_id) + COUNT(DISTINCT l.id) >', 0) // ← this line
|
||||
->orderBy('sal.created_at', 'DESC')
|
||||
->whereIn('sal.assigned_to', $sales_manager_ids)
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
// 6. activityBreakdown
|
||||
$total = $db->table('sales_activities')->countAll();
|
||||
|
||||
$activityBreakdown = $db->table('sales_activities')
|
||||
->select("activity_type, COUNT(*) AS total, ROUND(COUNT(*) * 100.0 / {$total}, 0) AS percentage", false)
|
||||
->groupBy('activity_type')
|
||||
->orderBy('total', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$teamPerformance = array_filter($teamPerformance, function($row) {
|
||||
return ($row['total_acts'] + $row['done_acts']) > 0;
|
||||
});
|
||||
|
||||
// 5. All Leads Overview
|
||||
$leadsOverview = $this->leadModel->select('sales_actual_leads.*, user_profiles.first_name, user_profiles.last_name')
|
||||
->join('user_profiles', 'user_profiles.id = sales_actual_leads.assigned_to', 'left')
|
||||
->findAll();
|
||||
|
||||
$data = [
|
||||
'total_leads' => $stats['total'],
|
||||
@ -825,7 +917,10 @@ class SalesController extends BaseController
|
||||
'pipeline_value' => '15.0L', // Hardcoded placeholder from PDF [cite: 14]
|
||||
'team' => $teamPerformance,
|
||||
'recent_acts' => $recentActivities,
|
||||
'leads_overview' => $leadsOverview
|
||||
'leads_overview' => $leadsOverview,
|
||||
'activity_breakdown'=> $activityBreakdown,
|
||||
'tab_name' => "Sales Dashboard",
|
||||
'page_name' => "Sales Dashboard"
|
||||
];
|
||||
|
||||
// dd($data);
|
||||
@ -839,16 +934,15 @@ class SalesController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function salesManagerLevelDashboard()
|
||||
public function salesManagerLevelDashboard($userId,$sales_manager_ids, $payload = [])
|
||||
{
|
||||
$userId = get_session_userid();
|
||||
// $userId = get_session_userid();
|
||||
// $userId = 1;
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
try {
|
||||
|
||||
$payload = $this->request->getGet();
|
||||
// $payload = $this->request->getGet();
|
||||
$current_fin_year = $payload['fy'] ?? getCurrentFinancialYear();
|
||||
|
||||
|
||||
@ -858,8 +952,12 @@ class SalesController extends BaseController
|
||||
->orderBy('fy_year', 'desc')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$fin_years = array_column($fin_years, 'fy_year');
|
||||
$fin_years[] = '2024-2025';
|
||||
|
||||
if(empty($fin_years)){
|
||||
$fin_years[] = $current_fin_year;
|
||||
}
|
||||
|
||||
$target = $db->table('sales_target')
|
||||
->where('user_id', $userId)
|
||||
@ -907,7 +1005,9 @@ class SalesController extends BaseController
|
||||
'recent_leads' => $recentLeads,
|
||||
'fin_years' => $fin_years,
|
||||
'display_fin_years' => format_financial_year($current_fin_year),
|
||||
'user_name' => get_session_userdata()->first_namee ?? ''
|
||||
'user_name' => get_session_userdata()->first_namee ?? '',
|
||||
'tab_name' => "Sales Dashboard",
|
||||
'page_name' => "Sales Dashboard"
|
||||
];
|
||||
|
||||
// dd($data);
|
||||
|
||||
@ -2077,7 +2077,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if (in_array(get_role_id(), [1, 2, 3, 5])) { ?>
|
||||
<?php if (in_array(get_role_id(), [1, 2, 3, 4]) || in_array(SALES_TEAM_ID, user_team())) { ?>
|
||||
<li class="li-seperate" id="app-sales-tracker-li">
|
||||
<a id="app_sales_tracker" href="#sidebarSalesTrackermenu" data-toggle="collapse" class="waves-effect img-inactive" style="color: grey;">
|
||||
<img
|
||||
@ -2087,13 +2087,13 @@
|
||||
<div class="collapse" id="sidebarSalesTrackermenu">
|
||||
<ul class="nav-second-level">
|
||||
<li>
|
||||
<a href="<?= base_url('/sales/page/Dashboard') ?>"> <i class="ri-dashboard-line"></i> Dashboard </a>
|
||||
<a href="<?= base_url('/sales/dashboard') ?>"> <i class="ri-dashboard-line"></i> Dashboard </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/sales') ?>"> <i class="ri-group-line"></i> Leads </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/sales/activities') ?>"><i class="ri-flashlight-line"></i> Activities </a>
|
||||
<a href="<?= base_url('/sales/loadactivities') ?>"><i class="ri-flashlight-line"></i> Activities </a>
|
||||
</li>
|
||||
<!-- <li>
|
||||
<a href="<?= base_url('/sales/page/Opportunities') ?>"> <i class="ri-checkbox-circle-line"></i> Opportunities </a>
|
||||
|
||||
@ -75,6 +75,7 @@
|
||||
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
||||
.status-pending { background: #fff3e0; color: #f57c00; }
|
||||
.status-completed { background: #e8f5e9; color: #388e3c; }
|
||||
.status-unknown { background: #000; color: #fff; }
|
||||
|
||||
/* 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; }
|
||||
@ -143,7 +144,7 @@
|
||||
|
||||
|
||||
</style>
|
||||
<!-- copy of tracker_view -->
|
||||
|
||||
<div class="main-content">
|
||||
<hr class="my-0">
|
||||
<div class="lead-header">
|
||||
@ -159,7 +160,7 @@
|
||||
<div class="lead-actions">
|
||||
<input type="text" class="search-input" id="mainSearch"
|
||||
placeholder="Search activities..." onkeyup="fetchActivities(false)">
|
||||
<button class="btn-primary" onclick="openModal('addActivityModal')">
|
||||
<button class="btn-primary" onclick="openMainActivityModal()">
|
||||
+ Add Activity
|
||||
</button>
|
||||
</div>
|
||||
@ -183,64 +184,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal" id="addActivityModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add Lead</h4>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('addActivityModal')">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="addLeadForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
<div class="modal-body p-4">
|
||||
<div class="form-group">
|
||||
<div class="col-12 mb-1">
|
||||
<div class="col-xl-12 col-lg-12 col-md-12">
|
||||
<label class="form-label">Company Name<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="company_name" style="width: 100%;" placeholder="Enter Company Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row mb-1">
|
||||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\-_+]/g, '')">
|
||||
</div>
|
||||
<div class="col-md-12" style="width: 94%;">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row mb-1">
|
||||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||||
<label class="form-label">Status<span class="text-danger">*</span></label>
|
||||
<select name="status" class="form-control" style="width: 100%;" required>
|
||||
<option value="New">New</option>
|
||||
<option value="Potential">Potential</option>
|
||||
<option value="Prospects">Prospects</option>
|
||||
<option value="Not a Prospects">Not a Prospects</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12" style="width: 94%;">
|
||||
<label class="form-label">Assign To<span class="text-danger">*</span></label>
|
||||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||||
<option value="">Select User</option>
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</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('addActivityModal')">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Create Lead</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="leadDetailModal">
|
||||
<div class="modal-content" style="max-width: 850px;">
|
||||
<div class="modal-header">
|
||||
@ -295,7 +238,7 @@
|
||||
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
<div class="modal-body p-4">
|
||||
|
||||
<input type="hidden" id="act_string_flag"> <!-- ADD THIS -->
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Select Lead <span class="text-danger">*</span></label>
|
||||
@ -306,15 +249,15 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Activity Type</label>
|
||||
<label class="form-label">Activity Type <span class="text-danger">*</span></label>
|
||||
<div class="activity-types" id="typeButtons">
|
||||
<button type="button" class="activity-type-btn active" onclick="selectType('Call', this)">📞 Call</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Email', this)">✉️ Email</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Meeting', this)">📅 Meeting</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Visit', this)">🚗 Visit</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Demo', this)">🎬 Demo</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Share', this)">📄 Share Docs</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Todo', this)">✓ To Do</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type active" data-type="Call" onclick="selectType('Call', this)">📞 Call</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Email" onclick="selectType('Email', this)">✉️ Email</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Meeting" onclick="selectType('Meeting', this)">📅 Meeting</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Visit" onclick="selectType('Visit', this)">🚗 Visit</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Demo" onclick="selectType('Demo', this)">🎬 Demo</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Share" onclick="selectType('Share', this)">📄 Share Docs</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Todo" onclick="selectType('Todo', this)">✓ To Do</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -362,6 +305,7 @@
|
||||
|
||||
<input type="hidden" id="comp_id">
|
||||
<input type="hidden" id="lead_id">
|
||||
<input type="hidden" id="string_flag">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Outcome / Notes <span class="text-danger">*</span></label>
|
||||
<textarea id="comp_notes" class="search-input" style="width:100%; height:80px;" required placeholder="What happened? Any next steps?" rows="3" required></textarea>
|
||||
@ -456,124 +400,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="editLeadModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Edit Lead</h4>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('editLeadModal')">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="editLeadForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
<input type="hidden" class="form-control" id="hidden_lead_id" name="lead_id">
|
||||
<div class="modal-body p-4">
|
||||
<div class="form-group">
|
||||
<div class="col-12 mb-1">
|
||||
<div class="col-xl-12 col-lg-12 col-md-12">
|
||||
<label class="form-label">Company Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="company_name" style="width: 100%;" placeholder="Enter Company Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row mb-1">
|
||||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\-_+]/g, '')">
|
||||
</div>
|
||||
<div class="col-md-12" style="width: 94%;">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mb-1">
|
||||
<div class="col-xl-12 col-lg-12 col-md-12">
|
||||
<label class="form-label">Address</label>
|
||||
<textarea id="address" name="address" class="form-control" style="width:100%; height:100px;" placeholder="Enter full Address"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row mb-1">
|
||||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||||
<label class="form-label">WebSite</label>
|
||||
<input type="text" class="form-control" name="website" style="width: 100%;" placeholder="Enter the Website">
|
||||
</div>
|
||||
<div class="col-md-12" style="width: 94%;">
|
||||
<label class="form-label">GST Number</label>
|
||||
<input type="text" class="form-control" name="gst_number" style="width: 100%;" placeholder="Enter the GST Number" oninput="this.value = this.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()" maxlength="15"
|
||||
pattern="^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-1" style="padding-left: 25px;">
|
||||
<label class="form-label">Contact Persons</label>
|
||||
<div id="contactPersonsContainer">
|
||||
<div id="savedContactsContainer"> <span id="NoData"> <center><i> No contact persons added yet </i> </center> </span> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-1 ml-1 ">
|
||||
|
||||
<div class="row g-2 align-items-center">
|
||||
|
||||
<div class="col-md-5" style="padding-left: 20px;">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="contact_name"
|
||||
placeholder="Contact Person Name"
|
||||
oninput="this.value=this.value.replace(/[^A-Za-z\s]/g,'')">
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mb-1">
|
||||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||||
<label class="form-label">Status <span class="text-danger">*</span></label>
|
||||
<select name="status" class="form-control" style="width: 100%;" required>
|
||||
<option value="New">New</option>
|
||||
<option value="Potential">Potential</option>
|
||||
<option value="Prospects">Prospects</option>
|
||||
<option value="Not a Prospects">Not a Prospects</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12" style="width: 94%;">
|
||||
<label class="form-label">Assign To <span class="text-danger">*</span> </label>
|
||||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||||
<option value="">Select User</option>
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</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('editLeadModal')">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Update Lead</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
resetFlatpicker();
|
||||
@ -605,6 +431,8 @@ function resetFlatpicker(){
|
||||
|
||||
const activityIcons = { Call: "📞", Email: "✉️", Meeting: "📅", Visit: "🚗",Demo: "🎬", Share: "📄", Todo: "✓" };
|
||||
const salesManagerIds = <?= json_encode($sales_manager_ids ?? []) ?>;
|
||||
console.log("***********************");
|
||||
console.log(salesManagerIds);return;
|
||||
const API = '<?= base_url('sales') ?>';
|
||||
let filter = 'all';
|
||||
let lead_id = null;
|
||||
@ -617,8 +445,6 @@ let currentOffset = 0;
|
||||
|
||||
|
||||
function openModal(id) { document.getElementById(id).classList.add('active'); resetFlatpicker(); }
|
||||
// function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||
|
||||
|
||||
function closeModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
@ -633,9 +459,9 @@ function closeModal(id) {
|
||||
|
||||
// Specific cleanup for your "Activity" logic
|
||||
if (id === 'activityModal') {
|
||||
selectedType = 'Call'; // Reset your global activity type variable
|
||||
$('.activity-type-btn').removeClass('active'); // Remove 'active' from all activity buttons
|
||||
$(`.activity-type-btn[data-type="${selectedType}"]`).addClass('active'); // Find the specific button for 'Call' and make it active
|
||||
selectedType = 'Call';
|
||||
$('#typeButtons .d_activity_type').removeClass('active');
|
||||
$('#typeButtons .d_activity_type[data-type="Call"]').addClass('active');
|
||||
}
|
||||
|
||||
// Specific cleanup for "Complete" modal (hidden follow-up sections)
|
||||
@ -649,18 +475,22 @@ function closeModal(id) {
|
||||
// Specific action for leadDetailModal
|
||||
if (id === 'leadDetailModal') {
|
||||
switchTab('activity'); // Reset the tab back to 'activity'
|
||||
document.getElementById('btn_add_opportunity').classList.style.display = 'none'; // Hide the button
|
||||
document.getElementById('btn_add_opportunity').style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openMainActivityModal() {
|
||||
document.getElementById('act_string_flag').value = "frommain"; // ✅ flag for main
|
||||
openModal('activityModal');
|
||||
}
|
||||
|
||||
function selectType(val, el) {
|
||||
document.querySelectorAll('.activity-type-btn').forEach(b => b.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
selectedType = val;
|
||||
}
|
||||
|
||||
|
||||
function selectFollowUpActivityType(val, el) {
|
||||
document.querySelectorAll('.f_activity_type').forEach(b => b.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
@ -675,7 +505,6 @@ function setFilter(val, el) {
|
||||
fetchActivities();
|
||||
}
|
||||
|
||||
|
||||
async function fetchActivities(isLoadMore = false) {
|
||||
const q = document.getElementById('mainSearch').value;
|
||||
const grid = document.getElementById('activitiesGrid');
|
||||
@ -692,6 +521,9 @@ async function fetchActivities(isLoadMore = false) {
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
console.log("function here");
|
||||
console.log(salesManagerIds);
|
||||
// return;
|
||||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
||||
if (typeof salesManagerIds === 'undefined' || salesManagerIds.length === 0) {
|
||||
console.log("No Sales Manager IDs found. Skipping API call.");
|
||||
@ -718,7 +550,7 @@ async function fetchActivities(isLoadMore = false) {
|
||||
}
|
||||
|
||||
// 5. Build URL with dynamic offset
|
||||
const 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!
|
||||
if (typeof salesManagerIds !== 'undefined' && salesManagerIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerIds.join(',')}`;
|
||||
@ -774,7 +606,7 @@ async function fetchActivities(isLoadMore = false) {
|
||||
</div>
|
||||
|
||||
<div class="activity-actions">
|
||||
<button class="btn-complete" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">
|
||||
<button style="display:${a.status == 'completed' ? 'none' : 'block'};" class="btn-complete" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id},'frommain')">
|
||||
✓ Complete
|
||||
</button>
|
||||
<button class="btn-view" onclick="viewDetail(${a.lead_id})">
|
||||
@ -815,13 +647,13 @@ async function fetchActivities(isLoadMore = false) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 2. Detail Logic
|
||||
async function viewDetail(id) {
|
||||
console.log("i am here");
|
||||
lead_id = id;
|
||||
const res = await fetch(`${API}/leads/${id}`);
|
||||
const json = await res.json();
|
||||
const l = json.data;
|
||||
let res = await fetch(`${API}/leads/${id}`);
|
||||
let json = await res.json();
|
||||
let l = json.data;
|
||||
|
||||
document.getElementById('det_company').innerText = l.company_name;
|
||||
document.getElementById('det_email').innerText = l.email || 'N/A';
|
||||
@ -843,8 +675,12 @@ async function viewDetail(id) {
|
||||
} else { document.getElementById('box_owner').style.display = 'none'; }
|
||||
|
||||
const badge = document.getElementById('det_status_badge');
|
||||
badge.innerText = l.status;
|
||||
badge.className = `lead-status status-${l.status.toLowerCase().replace(' ', '-')}`;
|
||||
// Use a fallback string like 'unknown' or 'pending'
|
||||
let status = l.status || 'unknown';
|
||||
|
||||
badge.innerText = status;
|
||||
badge.className = `lead-status status-${status.toLowerCase().replace(' ', '-')}`;
|
||||
|
||||
const oppBtn = document.getElementById('btn_add_opportunity');
|
||||
if (l.status === 'Prospects') {
|
||||
oppBtn.style.display = 'inline-block';
|
||||
@ -856,6 +692,7 @@ async function viewDetail(id) {
|
||||
renderCard(l.opportunities || []);
|
||||
openModal('leadDetailModal');
|
||||
}
|
||||
|
||||
function renderCard(opps) {
|
||||
const cont = document.getElementById('opportunitiesContainer');
|
||||
|
||||
@ -897,6 +734,7 @@ function renderCard(opps) {
|
||||
</div>`;
|
||||
}).join('') : `<div class="empty-state"> <div class="empty-icon">💼</div> No opportunities yet. Add one to get started!</div>`;
|
||||
}
|
||||
|
||||
function renderTimeline(acts) {
|
||||
const cont = document.getElementById('timelineContainer');
|
||||
|
||||
@ -919,8 +757,8 @@ function renderTimeline(acts) {
|
||||
hour12: true
|
||||
});
|
||||
|
||||
// Then use the variable in your HTML:
|
||||
// <span style="font-size:11px; color:#999">${formattedDate}</span>
|
||||
// Then use the variable in your HTML:
|
||||
// <span style="font-size:11px; color:#999">${formattedDate}</span>
|
||||
|
||||
return `
|
||||
<div class="timeline-item">
|
||||
@ -935,7 +773,7 @@ function renderTimeline(acts) {
|
||||
</div>
|
||||
<div style="font-size:13px; color:#444;"></div>
|
||||
${a.status === 'pending' ?
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:8px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:8px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id},'frompopup')">Mark Complete</button>` :
|
||||
`<div style="font-size:12px; color:#388e3c; margin-top:8px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||
}
|
||||
</div>
|
||||
@ -943,15 +781,16 @@ function renderTimeline(acts) {
|
||||
`;
|
||||
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</div>`;
|
||||
}
|
||||
|
||||
function openActivityModal() {
|
||||
document.getElementById('act_owner').value = global_lead_assigned_to;
|
||||
document.getElementById('act_owner').dispatchEvent(new Event('change'));
|
||||
document.getElementById('act_lead_id').value = lead_id;
|
||||
document.getElementById('act_lead_id').dispatchEvent(new Event('change'));
|
||||
openModal('activityModal');
|
||||
document.getElementById('act_string_flag').value = "frompopup";
|
||||
}
|
||||
|
||||
function openComp(id, assigned_to, lead_id) {
|
||||
function openComp(id, assigned_to, lead_id, string_flag) {
|
||||
document.getElementById('completeForm').reset();
|
||||
document.getElementById('f_notes').removeAttribute('required');
|
||||
document.getElementById('f_date').removeAttribute('required');
|
||||
@ -959,6 +798,7 @@ function openComp(id, assigned_to, lead_id) {
|
||||
document.getElementById('comp_id').value = id;
|
||||
document.getElementById('lead_id').value = lead_id;
|
||||
document.getElementById('f_assigned_to').value = assigned_to;
|
||||
document.getElementById('string_flag').value = string_flag;
|
||||
document.getElementById('f_assigned_to').dispatchEvent(new Event('change'));
|
||||
document.getElementById('f_typeButtons').querySelectorAll('.f_activity_type')
|
||||
.forEach(btn => btn.classList.remove('active'));
|
||||
@ -971,7 +811,6 @@ function openOpportunityModal() {
|
||||
openModal('opportunityModal');
|
||||
}
|
||||
|
||||
|
||||
function switchTab(tabName) {
|
||||
//Default Activity Tab how to resset here
|
||||
// 1. Hide all tab content
|
||||
@ -993,260 +832,6 @@ function switchTab(tabName) {
|
||||
document.getElementById('tab_' + tabName).classList.add('active');
|
||||
}
|
||||
|
||||
// Add Lead Form Submissions
|
||||
document.getElementById('addLeadForm').onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Just grab the button variables first, DO NOT disable yet
|
||||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.innerText;
|
||||
|
||||
const data = Object.fromEntries(new FormData(e.target).entries());
|
||||
|
||||
let companyName = data.company_name?.trim();
|
||||
let email = data.email?.trim();
|
||||
let phone = data.phone?.trim();
|
||||
|
||||
let companyRegex = /^[A-Za-z\s]+$/;
|
||||
let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
let phoneRegex = /^\+?[0-9\s]{10,20}$/; // Phone regex (+, numbers, spaces allowed, 10-20 length)
|
||||
|
||||
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
|
||||
function showError(message, fieldName) {
|
||||
toastr.warning(message, 'Validation Error');
|
||||
setTimeout(function() {
|
||||
$('[name="' + fieldName + '"]').focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// --- VALIDATION CHECKS (Button is still normal here) ---
|
||||
if (!companyName && !email && !phone && !data.assigned_to) {
|
||||
toastr.warning('Please fill in all the required fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyName) {
|
||||
return showError('Company Name is required.', 'company_name');
|
||||
}
|
||||
|
||||
if (!companyRegex.test(companyName)) {
|
||||
return showError('Company Name can contain only letters and spaces.', 'company_name');
|
||||
}
|
||||
|
||||
if (companyName.length < 2) {
|
||||
return showError('Company Name must be at least 2 characters.', 'company_name');
|
||||
}
|
||||
|
||||
if (!email && !phone) {
|
||||
return showError("Either Email or Phone number is required.", 'email');
|
||||
}
|
||||
|
||||
if (email && !emailRegex.test(email)) {
|
||||
return showError("Please enter a valid email address.", 'email');
|
||||
}
|
||||
|
||||
if (phone && !phoneRegex.test(phone)) {
|
||||
return showError("Phone number can contain only +, numbers and spaces (10–20 digits).", 'phone');
|
||||
}
|
||||
|
||||
if (!data.status) {
|
||||
return showError("Please select a status.", 'status');
|
||||
}
|
||||
|
||||
if (!data.assigned_to) {
|
||||
return showError("Please Select the user to be Assigned.", 'assigned_to');
|
||||
}
|
||||
|
||||
// --- ALL VALIDATION PASSED: Now disable button ---
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = 'Creating...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/leads`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if(res.ok) {
|
||||
toastr.success('Leads Created Successfully');
|
||||
closeModal('addActivityModal');
|
||||
fetchActivities();
|
||||
e.target.reset();
|
||||
$(e.target).find('.searchable').trigger('change');
|
||||
} else {
|
||||
let err = await res.json();
|
||||
if (res.status === 400) {
|
||||
let errorMessages = "";
|
||||
let seenMessages = [];
|
||||
let isFirstError = true;
|
||||
$('.form-control').removeClass('is-invalid');
|
||||
|
||||
if (err.messages) {
|
||||
Object.entries(err.messages).forEach(([field, message]) => {
|
||||
let inputElement = $('[name="' + field + '"]');
|
||||
|
||||
if (inputElement.length > 0) {
|
||||
inputElement.addClass('is-invalid');
|
||||
if (isFirstError) {
|
||||
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(err.message || 'Validation failed', 'Warning');
|
||||
}
|
||||
} else {
|
||||
toastr.error(err.message || 'Error adding lead');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toastr.error("A network error occurred.");
|
||||
} finally {
|
||||
// ALWAYS runs to reset button
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = originalBtnText;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Edit Lead Form Submissions
|
||||
document.getElementById('editLeadForm').onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Just grab the button variables first, DO NOT disable yet
|
||||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.innerText;
|
||||
|
||||
const data = Object.fromEntries(new FormData(e.target).entries());
|
||||
console.log("Lead :", data);
|
||||
|
||||
let leadId = data.lead_id?.trim();
|
||||
let companyName = data.company_name?.trim();
|
||||
let email = data.email?.trim();
|
||||
let phone = data.phone?.trim();
|
||||
let gst = data.gst_number?.trim();
|
||||
|
||||
let companyRegex = /^[A-Za-z\s]+$/;
|
||||
let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
let phoneRegex = /^\+?[0-9\s]{10,20}$/;
|
||||
let gstRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/;
|
||||
|
||||
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
|
||||
function showError(message, fieldName) {
|
||||
toastr.warning(message, 'Validation Error');
|
||||
setTimeout(function() {
|
||||
$('[name="' + fieldName + '"]').focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// --- VALIDATION CHECKS (Button is still normal here) ---
|
||||
if (!companyName && !email && !phone && !data.assigned_to) {
|
||||
toastr.warning('Please fill in all the required fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyName) {
|
||||
return showError('Company Name is required.', 'company_name');
|
||||
}
|
||||
|
||||
if (!companyRegex.test(companyName)) {
|
||||
return showError('Company Name can contain only letters and spaces.', 'company_name');
|
||||
}
|
||||
|
||||
if (companyName.length < 2) {
|
||||
return showError('Company Name must be at least 2 characters.', 'company_name');
|
||||
}
|
||||
|
||||
if (!email && !phone) {
|
||||
return showError("Either Email or Phone number is required.", 'email');
|
||||
}
|
||||
|
||||
if (email && !emailRegex.test(email)) {
|
||||
return showError("Please enter a valid email address.", 'email');
|
||||
}
|
||||
|
||||
if (phone && !phoneRegex.test(phone)) {
|
||||
return showError("Phone number can contain only +, numbers and spaces (10–20 digits).", 'phone');
|
||||
}
|
||||
|
||||
if (gst && !gstRegex.test(gst)) {
|
||||
return showError("Please enter a valid GST Number (e.g., 22AAAAA0000A1Z5).", 'gst_number');
|
||||
}
|
||||
|
||||
if (!data.status) {
|
||||
return showError("Please select a status.", 'status');
|
||||
}
|
||||
|
||||
if (!data.assigned_to) {
|
||||
return showError("Please Select the user to be Assigned.", 'assigned_to');
|
||||
}
|
||||
|
||||
// --- ALL VALIDATION PASSED: Now disable button ---
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = 'Updating...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/leads/${leadId}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if(res.ok) {
|
||||
toastr.success('Leads Updated Successfully');
|
||||
closeModal('editLeadModal');
|
||||
fetchActivities();
|
||||
e.target.reset();
|
||||
$('#savedContactsContainer').empty();
|
||||
$('#NoData').show();
|
||||
} else {
|
||||
let err = await res.json();
|
||||
if (res.status === 400) {
|
||||
let errorMessages = "";
|
||||
let seenMessages = [];
|
||||
let isFirstError = true;
|
||||
$('.form-control').removeClass('is-invalid');
|
||||
|
||||
if (err.messages) {
|
||||
Object.entries(err.messages).forEach(([field, message]) => {
|
||||
let inputElement = $('[name="' + field + '"]');
|
||||
|
||||
if (inputElement.length > 0) {
|
||||
inputElement.addClass('is-invalid');
|
||||
if (isFirstError) {
|
||||
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(err.message || 'Validation failed', 'Warning');
|
||||
}
|
||||
} else {
|
||||
toastr.error(err.message || 'Error updating lead');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toastr.error("A network error occurred.");
|
||||
} finally {
|
||||
// ALWAYS runs to reset button
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = originalBtnText;
|
||||
}
|
||||
};
|
||||
|
||||
// Activity Form Submissions
|
||||
document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@ -1256,6 +841,8 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
let actNotes = document.getElementById('act_notes').value.trim();
|
||||
let actOwner = document.getElementById('act_owner').value.trim();
|
||||
let actLead = document.getElementById('act_lead_id').value.trim();
|
||||
let flag = document.getElementById('act_string_flag').value;
|
||||
|
||||
|
||||
// 2. Perform Validation
|
||||
if (!actLead) {
|
||||
@ -1268,8 +855,13 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasDefaultActiveType = document.querySelector('#typeButtons .d_activity_type.active');
|
||||
if (!hasDefaultActiveType) {
|
||||
toastr.warning('Please select an activity type.');
|
||||
return;
|
||||
}
|
||||
if (!selectedType) {
|
||||
toastr.warning('Please select activity type');
|
||||
toastr.warning('Please select an activity type.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1311,7 +903,11 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
if (res.ok) {
|
||||
toastr.success('Activity Created Successfully');
|
||||
closeModal('activityModal');
|
||||
viewDetail(document.getElementById('act_lead_id').value); // Make sure lead_id is passed correctly
|
||||
if (flag === "frompopup") {
|
||||
viewDetail(document.getElementById('act_lead_id').value);
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
toastr.error('Failed to create activity.');
|
||||
}
|
||||
@ -1329,6 +925,8 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
// Activity Timeline - complete Form Submissions
|
||||
document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let flag = document.getElementById('string_flag').value; //"frompopup"
|
||||
|
||||
// 1. Grab button variables first, DO NOT disable yet
|
||||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||||
@ -1354,9 +952,14 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
let fDate = document.getElementById('f_date').value.trim();
|
||||
let fAssigned = document.getElementById('f_assigned_to').value.trim();
|
||||
|
||||
const hasActiveType = document.querySelector('#f_typeButtons .f_activity_type.active');
|
||||
if (!hasActiveType) {
|
||||
toastr.warning('Please select the next activity type.');
|
||||
return;
|
||||
}
|
||||
// FIX: Check your global variable, not the undefined DOM element
|
||||
if (!selectedFollowUpActivityType) {
|
||||
toastr.warning('Please Pickup Next Activity Type');
|
||||
toastr.warning('Please select the next activity type.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1425,7 +1028,13 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
|
||||
// Final Actions
|
||||
closeModal('completeModal');
|
||||
viewDetail(leadId);
|
||||
|
||||
if (flag === "frompopup") {
|
||||
viewDetail(leadId);
|
||||
} else if (flag === "frommain") {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
e.target.reset();
|
||||
selectedFollowUpActivityType = ''; // Reset global variable
|
||||
|
||||
@ -1458,6 +1067,7 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
submitBtn.innerText = originalBtnText;
|
||||
}
|
||||
};
|
||||
|
||||
function submitToRedirectwithactualLeadIDUrl(){
|
||||
|
||||
|
||||
@ -1516,16 +1126,26 @@ document.getElementById('do_follow').addEventListener('change', function () {
|
||||
});
|
||||
|
||||
document.getElementById('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('lead_id').value.trim();
|
||||
e.preventDefault(); // Prevent accidental form submission
|
||||
|
||||
// 1. Get and Trim Values
|
||||
const nameInput = document.getElementById('contact_name');
|
||||
const mobileInput = document.getElementById('contact_mobile');
|
||||
const leadIdInput = document.getElementById('lead_id');
|
||||
|
||||
const name = nameInput.value.trim();
|
||||
const mobile = mobileInput.value.trim();
|
||||
const lead_id = leadIdInput.value.trim();
|
||||
|
||||
// 2. Simple Client-Side Validation
|
||||
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 };
|
||||
// Reset previous error states
|
||||
document.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
|
||||
|
||||
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/contacts`, {
|
||||
@ -1534,19 +1154,19 @@ document.getElementById('btnSaveContact').onclick = async (e) => {
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await res.json(); // Get the response body
|
||||
const result = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
// Clear inputs
|
||||
document.getElementById('contact_name').value = '';
|
||||
document.getElementById('contact_mobile').value = '';
|
||||
// Success: Clear inputs
|
||||
nameInput.value = '';
|
||||
mobileInput.value = '';
|
||||
|
||||
// Hide "No Data" message
|
||||
// Hide "No Data" message if it exists
|
||||
const noData = document.getElementById('NoData');
|
||||
if(noData) noData.style.display = 'none';
|
||||
|
||||
// Create the HTML string
|
||||
let contactHtml = `
|
||||
// Generate HTML for the new contact
|
||||
const contactHtml = `
|
||||
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-2"
|
||||
style="background: #f8f8f8; border-radius: 8px;"
|
||||
data-id="${result.data.contact_id}">
|
||||
@ -1554,45 +1174,44 @@ document.getElementById('btnSaveContact').onclick = async (e) => {
|
||||
<div class="fw-bold">${result.data.name}</div>
|
||||
<div class="text-muted">${result.data.mobile}</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-danger btn-sm btnRemoveContact"
|
||||
data-id="${result.data.contact_id}">
|
||||
Remove
|
||||
</button>
|
||||
<div>
|
||||
<button type="button"
|
||||
class="btn btn-danger btn-sm btnRemoveContact"
|
||||
data-id="${result.data.contact_id}">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// <button type="button"
|
||||
// <button type="button"
|
||||
// class="btn btn-secondary btn-sm btnEditContact"
|
||||
// data-id="${result.data.contact_id}">
|
||||
// Update
|
||||
// </button>
|
||||
// Append to container using Vanilla JS
|
||||
// Append to container
|
||||
document.getElementById('savedContactsContainer').insertAdjacentHTML('beforeend', contactHtml);
|
||||
toastr.success('Contact saved successfully');
|
||||
} else {
|
||||
let err = await res.json();
|
||||
// Handle Errors using the 'result' we already parsed
|
||||
if (res.status === 400) {
|
||||
let errorMessages = "";
|
||||
let seenMessages = [];
|
||||
if (err.messages) {
|
||||
Object.entries(err.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');
|
||||
let isFirstError = true; // Fixed: Variable initialization
|
||||
|
||||
// Focus on the first error field
|
||||
if (result.messages) {
|
||||
Object.entries(result.messages).forEach(([field, message]) => {
|
||||
// Find element by name attribute (Vanilla JS)
|
||||
let inputElement = document.querySelector(`[name="${field}"]`);
|
||||
|
||||
if (inputElement) {
|
||||
inputElement.classList.add('is-invalid');
|
||||
if (isFirstError) {
|
||||
// The 100ms delay safely bypasses Bootstrap's modal focus block
|
||||
setTimeout(function() {
|
||||
inputElement.focus();
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => inputElement.focus(), 100);
|
||||
isFirstError = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!seenMessages.includes(message)) {
|
||||
errorMessages += `• ${message}<br>`;
|
||||
seenMessages.push(message);
|
||||
@ -1600,16 +1219,15 @@ document.getElementById('btnSaveContact').onclick = async (e) => {
|
||||
});
|
||||
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
|
||||
} else {
|
||||
toastr.warning(err.message || 'Validation failed', 'Warning');
|
||||
toastr.warning(result.message || 'Validation failed');
|
||||
}
|
||||
} else {
|
||||
toastr.error(result.message || 'An error occurred while saving.');
|
||||
}
|
||||
else {
|
||||
toastr.error(err.message || 'Error adding lead');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toastr.error('An error occurred');
|
||||
console.error("Fetch Error:", error);
|
||||
toastr.error('Network error or server is unreachable.');
|
||||
}
|
||||
};
|
||||
|
||||
@ -1645,73 +1263,6 @@ document.getElementById('savedContactsContainer').onclick = async (e) => {
|
||||
}
|
||||
};
|
||||
|
||||
async function openEditLeadModal(id) {
|
||||
// 1. Reset UI State
|
||||
document.getElementById('hidden_lead_id').value = id;
|
||||
const container = $('#savedContactsContainer');
|
||||
// Remove only previous contact cards, keep the NoData span for now
|
||||
container.find('.contact-card').remove();
|
||||
|
||||
try {
|
||||
// 2. Fetch Lead Details
|
||||
const response = await fetch(`${API}/leads/${id}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch lead');
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// IMPORTANT: Your data is nested inside result.data
|
||||
const lead = result.data;
|
||||
|
||||
// 3. Populate Form Fields
|
||||
const form = document.getElementById('editLeadForm');
|
||||
|
||||
// Mapping fields carefully
|
||||
form.querySelector('[name="company_name"]').value = lead.company_name || '';
|
||||
form.querySelector('[name="email"]').value = lead.email || '';
|
||||
form.querySelector('[name="phone"]').value = lead.phone || '';
|
||||
form.querySelector('[name="address"]').value = lead.address || '';
|
||||
form.querySelector('[name="website"]').value = lead.website || '';
|
||||
form.querySelector('[name="gst_number"]').value = lead.gst_number || '';
|
||||
form.querySelector('[name="status"]').value = lead.status || 'New';
|
||||
form.querySelector('[name="assigned_to"]').value = lead.assigned_to || '';
|
||||
$(form.querySelector('[name="assigned_to"]')).trigger('change');
|
||||
|
||||
// 4. Handle Contact Persons (Looping through the nested array)
|
||||
const contacts = lead.contact_persons; // Array from your JSON
|
||||
|
||||
if (contacts && contacts.length > 0) {
|
||||
$('#NoData').hide();
|
||||
|
||||
contacts.forEach(contact => {
|
||||
let contactHtml = `
|
||||
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-3"
|
||||
style="background: #f8f8f8; border-radius: 8px;"
|
||||
data-id="${contact.contact_id}">
|
||||
<div>
|
||||
<div class="fw-bold">${contact.name}</div>
|
||||
<div class="text-muted">${contact.mobile}</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-danger btn-sm btnRemoveContact"
|
||||
data-id="${contact.contact_id}">
|
||||
Remove
|
||||
</button>
|
||||
</div>`;
|
||||
container.append(contactHtml);
|
||||
});
|
||||
} else {
|
||||
$('#NoData').show();
|
||||
}
|
||||
|
||||
// 5. Open the Modal
|
||||
openModal('editLeadModal');
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading lead data:", error);
|
||||
alert("Could not load lead details. Check console for details.");
|
||||
}
|
||||
}
|
||||
|
||||
function convertDBFormatted(input) {
|
||||
|
||||
if (!input) return null;
|
||||
@ -1739,6 +1290,7 @@ function convertDBFormatted(input) {
|
||||
seconds.padStart(2, '0')
|
||||
);
|
||||
}
|
||||
|
||||
fetchActivities();
|
||||
|
||||
</script>
|
||||
@ -9,6 +9,38 @@
|
||||
.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; }
|
||||
|
||||
@ -29,10 +61,18 @@
|
||||
.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 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>
|
||||
@ -40,29 +80,34 @@
|
||||
<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> -->
|
||||
|
||||
<div class="stat-cards">
|
||||
<div class="card">
|
||||
<div class="stat-val">4</div>
|
||||
<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 class="stat-change text-success">↑ 12% this month</div> -->
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="stat-val">6</div>
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo $total_activities ?></div>
|
||||
<div class="stat-label">Total Activities</div>
|
||||
<div class="stat-change text-success">↑ 5% this month</div>
|
||||
<!-- <div class="stat-change text-success">↑ 5% this month</div> -->
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="stat-val">1</div>
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo ($total_activities - $completed_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 $completed_acts ?></div>
|
||||
<div class="stat-label">Completed Activities</div>
|
||||
<div class="stat-change text-success">↑ 15% this month</div>
|
||||
<!-- <div class="stat-change text-success">↑ 15% this month</div> -->
|
||||
</div>
|
||||
<div class="card">
|
||||
<!-- <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>
|
||||
|
||||
<div class="main-grid">
|
||||
@ -81,62 +126,117 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Venba Infotech</strong></td>
|
||||
<td>
|
||||
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">CALL</div>
|
||||
<div style="font-size: 12px; color: #718096;">John Doe</div>
|
||||
</td>
|
||||
<td style="color: #4a5568; font-weight: 500;">20 Feb 2026</td>
|
||||
<td><span class="status-pill status-completed">Completed</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Acme Corporation</strong></td>
|
||||
<td>
|
||||
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">EMAIL</div>
|
||||
<div style="font-size: 12px; color: #718096;">John Doe</div>
|
||||
</td>
|
||||
<td style="color: #4a5568; font-weight: 500;">20 Feb 2026</td>
|
||||
<td><span class="status-pill status-pending">Pending</span></td>
|
||||
</tr>
|
||||
<?php if (!empty($recent_acts)): ?>
|
||||
<?php foreach ($recent_acts as $a): ?>
|
||||
<?php
|
||||
$activityIcons = [
|
||||
'Call' => '📞',
|
||||
'Email' => '✉️',
|
||||
'Meeting' => '📅',
|
||||
'Visit' => '🚗',
|
||||
'Demo' => '🎬',
|
||||
'Share' => '📄',
|
||||
'Todo' => '✓'
|
||||
];
|
||||
$icon = $activityIcons[$a['activity_type']] ?? '📌';
|
||||
$statusClass = strtolower(str_replace(' ', '-', $a['status']));
|
||||
$formattedDate = date('d M Y', 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>
|
||||
</td>
|
||||
<td style="color: #4a5568; font-weight: 500;"><?= $formattedDate ?></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>
|
||||
<div class="team-member">
|
||||
<div class="member-img" style="background: #ff6b35;">V</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 700; font-size: 14px;">Venkat 3.0</div>
|
||||
<div style="font-size: 11px; color: #718096;">Sales Manager</div>
|
||||
</div>
|
||||
<div class="act-stats">
|
||||
<div class="total">6 acts</div>
|
||||
<div class="done">1 done</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-member">
|
||||
<div class="member-img" style="background: #4a5568;">P</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 700; font-size: 14px;">Pavi_the_Staff V</div>
|
||||
<div style="font-size: 11px; color: #718096;">Sales Staff</div>
|
||||
</div>
|
||||
<div class="act-stats">
|
||||
<div class="total">0 acts</div>
|
||||
<div class="done">0 done</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?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: #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>
|
||||
<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' => ['color' => '#ed8936', 'icon' => '📄'],
|
||||
'Todo' => ['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 class="dot" style="background: <?= $color ?>;"></span>
|
||||
<?= $icon ?> <?= esc($type) ?>
|
||||
</span>
|
||||
<strong><?= $item['percentage'] ?>%</strong>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="empty-state">No activities found</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -157,20 +257,24 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Venba Infotech</strong></td>
|
||||
<td><span style="background: #edf2f7; color: #4a5568; font-size: 10px; padding: 3px 10px; border-radius: 12px; font-weight: 700; text-transform: uppercase;">New</span></td>
|
||||
<td style="color: #4a5568;">John Doe</td>
|
||||
<td><div style="text-align: center; font-weight: 700;">1</div></td>
|
||||
<td><div style="text-align: center; font-weight: 700;">1</div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Acme Corporation</strong></td>
|
||||
<td><span style="background: #fff3e0; color: #f57c00; font-size: 10px; padding: 3px 10px; border-radius: 12px; font-weight: 700; text-transform: uppercase;">Potential</span></td>
|
||||
<td style="color: #4a5568;">John Doe</td>
|
||||
<td><div style="text-align: center; font-weight: 700;">1</div></td>
|
||||
<td><div style="text-align: center; font-weight: 700;">0</div></td>
|
||||
</tr>
|
||||
<?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">No leads found</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -19,6 +19,35 @@
|
||||
|
||||
.lead-initial { width: 35px; height: 35px; background: #f0f0f0; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #ff6b35; }
|
||||
.status-pill { font-size: 10px; padding: 2px 10px; border-radius: 12px; font-weight: 600; }
|
||||
|
||||
/* 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.active { display: flex; }
|
||||
.modal-content { background: white; border-radius: 12px; width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; }
|
||||
.modal-header { padding: 5px 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; }
|
||||
.modal-body { padding: 25px; flex: 1; }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
.form-label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
|
||||
|
||||
.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; }
|
||||
input, select, textarea { background-color: white !important; border-radius: 6px !important; box-shadow: none !important; border-color: #ddd !important; }
|
||||
input, select, textarea { width: 400px; padding: 10px 15px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; outline: none; }
|
||||
input:focus, select:focus, textarea:focus { border-color: #ff6b35; outline: none; /* removes blue browser outline */ }
|
||||
.modal .form-control { transition: 0.2s ease; }
|
||||
|
||||
.modal .form-control:focus { border-color: #ff6b35; box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-primary { background: #ff6b35; 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: #ff5722; transform: translateY(-1px); }
|
||||
.btn-complete { background: #4caf50; color: white; border: none;padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
|
||||
.btn-complete:hover { background: #4caf50; transform: translateY(-1px); }
|
||||
.btn-view { background: #f0f0f0; color: #666; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
|
||||
.btn-view:hover { background: #f0f0f0; transform: translateY(-1px); }
|
||||
|
||||
</style>
|
||||
|
||||
<div class="my-dash">
|
||||
@ -82,7 +111,7 @@
|
||||
<div style="font-weight:600; font-size:14px; color: #333;"><?= $u['company_name'] ?></div>
|
||||
<div style="font-size:11px; color:#999; margin-top: 3px;"><?= strtoupper($u['activity_type'] ?? 'EMAIL') ?> - <?= date('d Mar Y', strtotime($u['scheduled_date'])) ?></div>
|
||||
</div>
|
||||
<button class="badge-done">✓ Mark as completed</button>
|
||||
<button class="badge-done" onclick="openComp(<?= $u['activity_id'] ?>,<?= $u['lead_id'] ?>)">✓ Mark as completed</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
@ -105,20 +134,141 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="completeModal">
|
||||
<div class="modal-content" style="max-width: 675px;">
|
||||
<div class="modal-header">
|
||||
<h3>Complete Activity</h3>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('completeModal')">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
<div class="modal-body p-4">
|
||||
|
||||
<input type="hidden" id="comp_id">
|
||||
<input type="hidden" id="lead_id">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Outcome / Notes <span class="text-danger">*</span></label>
|
||||
<textarea id="comp_notes" class="search-input" style="width:100%; height:80px;" required placeholder="What happened? Any next steps?" rows="3" 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('completeModal')">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Submit Outcome</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const fy = params.get('fy');
|
||||
if (fy) {
|
||||
document.getElementById('financial_year').value = fy;
|
||||
}
|
||||
});
|
||||
const API = '<?= base_url('sales') ?>';
|
||||
function openModal(id) { document.getElementById(id).classList.add('active'); }
|
||||
|
||||
function onFinancialYearChange(select) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('fy', select.value);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
function openComp(id, lead_id) {
|
||||
|
||||
document.getElementById('completeForm').reset();
|
||||
document.getElementById('comp_id').value = id;
|
||||
document.getElementById('lead_id').value = lead_id;
|
||||
openModal('completeModal');
|
||||
}
|
||||
document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 1. Grab button variables first, DO NOT disable yet
|
||||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.innerText;
|
||||
|
||||
// 2. Capture values into variables
|
||||
const activityId = document.getElementById('comp_id').value;
|
||||
const leadId = document.getElementById('lead_id').value;
|
||||
|
||||
// This creates a TRUE or FALSE boolean
|
||||
let compNotes = document.getElementById('comp_notes').value.trim();
|
||||
|
||||
// --- VALIDATION CHECKS (Button is still normal here) ---
|
||||
if (compNotes === '') {
|
||||
toastr.warning('Completion notes is required');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- ALL VALIDATION PASSED: Now disable button ---
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = 'Submitting...';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
completion_notes: compNotes, // Use the already trimmed variable
|
||||
create_followup: 'no',
|
||||
};
|
||||
|
||||
// Mark current activity complete
|
||||
const res = await fetch(`${API}/activities/${activityId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success('Updated Successfully');
|
||||
// Final Actions
|
||||
closeModal('completeModal');
|
||||
|
||||
e.target.reset();
|
||||
|
||||
window.location.reload();
|
||||
|
||||
} else {
|
||||
let err = await res.json();
|
||||
if (res.status === 400) {
|
||||
let errorMessages = "";
|
||||
let seenMessages = [];
|
||||
|
||||
if (err.messages) {
|
||||
Object.entries(err.messages).forEach(([field, message]) => {
|
||||
if (!seenMessages.includes(message)) {
|
||||
errorMessages += `• ${message}<br>`;
|
||||
seenMessages.push(message);
|
||||
}
|
||||
});
|
||||
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
|
||||
} else {
|
||||
toastr.warning(err.message || 'Validation failed', 'Warning');
|
||||
}
|
||||
} else {
|
||||
toastr.error(err.message || 'Error completing activity');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toastr.error("A network error occurred.");
|
||||
} finally {
|
||||
// ALWAYS runs to reset button
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = originalBtnText;
|
||||
}
|
||||
};
|
||||
|
||||
function closeModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) {
|
||||
modal.classList.remove('active'); // Hide the modal
|
||||
const form = modal.querySelector('form'); // Find the form inside this specific modal
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const fy = params.get('fy');
|
||||
if (fy) {
|
||||
document.getElementById('financial_year').value = fy;
|
||||
}
|
||||
});
|
||||
|
||||
function onFinancialYearChange(select) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('fy', select.value);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
</script>
|
||||
@ -276,15 +276,17 @@
|
||||
<div class="modal-body p-4">
|
||||
|
||||
<input type="hidden" id="act_lead_id">
|
||||
<label class="form-label">Activity Type</label>
|
||||
<label class="form-label">Activity Type <span class="text-danger">*</span></label>
|
||||
<div class="activity-types" id="typeButtons">
|
||||
<button type="button" class="activity-type-btn active" onclick="selectType('Call', this)">📞 Call</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Email', this)">✉️ Email</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Meeting', this)">📅 Meeting</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Visit', this)">🚗 Visit</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Demo', this)">🎬 Demo</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Share', this)">📄 Share Docs</button>
|
||||
<button type="button" class="activity-type-btn" onclick="selectType('Todo', this)">✓ To Do</button>
|
||||
|
||||
<button type="button" class="activity-type-btn d_activity_type active" data-type="Call" onclick="selectType('Call', this)">📞 Call</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Email" onclick="selectType('Email', this)">✉️ Email</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Meeting" onclick="selectType('Meeting', this)">📅 Meeting</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Visit" onclick="selectType('Visit', this)">🚗 Visit</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Demo" onclick="selectType('Demo', this)">🎬 Demo</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Share" onclick="selectType('Share', this)">📄 Share Docs</button>
|
||||
<button type="button" class="activity-type-btn d_activity_type" data-type="Todo" onclick="selectType('Todo', this)">✓ To Do</button>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Notes <span class="text-danger">*</span></label>
|
||||
@ -650,11 +652,11 @@ function closeModal(id) {
|
||||
|
||||
// Specific cleanup for your "Activity" logic
|
||||
if (id === 'activityModal') {
|
||||
selectedType = 'Call'; // Reset your global activity type variable
|
||||
$('.activity-type-btn').removeClass('active'); // Remove 'active' from all activity buttons
|
||||
$(`.activity-type-btn[data-type="${selectedType}"]`).addClass('active'); // Find the specific button for 'Call' and make it active
|
||||
selectedType = 'Call';
|
||||
$('#typeButtons .d_activity_type').removeClass('active');
|
||||
$('#typeButtons .d_activity_type[data-type="Call"]').addClass('active'); // ✅
|
||||
}
|
||||
|
||||
|
||||
// Specific cleanup for "Complete" modal (hidden follow-up sections)
|
||||
if (id === 'completeModal') {
|
||||
const fUpSection = document.getElementById('f_up');
|
||||
@ -666,7 +668,7 @@ function closeModal(id) {
|
||||
// Specific action for leadDetailModal
|
||||
if (id === 'leadDetailModal') {
|
||||
switchTab('activity'); // Reset the tab back to 'activity'
|
||||
document.getElementById('btn_add_opportunity').classList.style.display = 'none'; // Hide the button
|
||||
document.getElementById('btn_add_opportunity').style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1294,11 +1296,15 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
toastr.warning('Notes is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedType) {
|
||||
toastr.warning('Please select activity type');
|
||||
const hasDefaultActiveType = document.querySelector('#typeButtons .d_activity_type.active');
|
||||
if (!hasDefaultActiveType) {
|
||||
toastr.warning('Please select an activity type.');
|
||||
return;
|
||||
}
|
||||
if (!selectedType) {
|
||||
toastr.warning('Please select an activity type.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!feFormat.trim()) {
|
||||
toastr.warning('Scheduled date & time is required');
|
||||
@ -1337,6 +1343,11 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success('Activity Created Successfully');
|
||||
selectedType = 'Call'; // Reset your global activity type variable
|
||||
// Remove 'active' from all activity buttons
|
||||
$('.activity-type-btn').removeClass('active');
|
||||
// Add 'active' to the specific button using backticks for the template literal
|
||||
$(`.activity-type-btn[onclick*="'${selectedType}'"]`).addClass('active');
|
||||
closeModal('activityModal');
|
||||
viewDetail(document.getElementById('act_lead_id').value); // Make sure lead_id is passed correctly
|
||||
} else {
|
||||
@ -1382,8 +1393,13 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
let fAssigned = document.getElementById('f_assigned_to').value.trim();
|
||||
|
||||
// FIX: Check your global variable, not the undefined DOM element
|
||||
const hasActiveType = document.querySelector('#f_typeButtons .f_activity_type.active');
|
||||
if (!hasActiveType) {
|
||||
toastr.warning('Please select the next activity type.');
|
||||
return;
|
||||
}
|
||||
if (!selectedFollowUpActivityType) {
|
||||
toastr.warning('Please Pickup Next Activity Type');
|
||||
toastr.warning('Please select the next activity type.');
|
||||
return;
|
||||
}
|
||||
// if (!selectedFollowUpActivityType) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user