FIX_SalesTracker_QA 1
This commit is contained in:
parent
95285dd602
commit
39a6c29fb2
@ -1038,6 +1038,10 @@ $routes->group('sales', function($routes) {
|
||||
$routes->get('dashboard', 'SalesController::dashboard');
|
||||
$routes->get('branchLevelDashboard', 'SalesController::branchLevelDashboard');
|
||||
$routes->get('salesManagerLevelDashboard', 'SalesController::salesManagerLevelDashboard');
|
||||
|
||||
//Validations
|
||||
$routes->get('checkDuplicate', 'SalesController::checkDuplicate');
|
||||
$routes->get('searchClients', 'SalesController::searchClients');
|
||||
});
|
||||
|
||||
// Expence Module Route Group
|
||||
|
||||
@ -4629,8 +4629,9 @@ class LeadsController extends BaseController
|
||||
|
||||
// 🔹 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)
|
||||
->select('sales_actual_leads.company_name, clients.short_name, sales_actual_leads.email, sales_actual_leads.phone, sales_actual_leads.address, sales_actual_leads.website, sales_actual_leads.gst_number, sales_actual_leads.status, sales_actual_leads.assigned_to')
|
||||
->join('clients', 'clients.id = sales_actual_leads.client_id', 'left')
|
||||
->where('sales_actual_leads.lead_id', $actual_lead_id)
|
||||
->first(); // first row only
|
||||
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ use App\Models\SalesActivityModel;
|
||||
use App\Models\SalesLeadNoteModel;
|
||||
use App\Models\SalesTargetModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\ClientModel;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
@ -22,6 +23,7 @@ class SalesController extends BaseController
|
||||
protected $noteModel;
|
||||
protected $userModel;
|
||||
protected $targetModel;
|
||||
protected $clientModel;
|
||||
|
||||
|
||||
public function __construct()
|
||||
@ -32,6 +34,7 @@ class SalesController extends BaseController
|
||||
$this->noteModel = new SalesLeadNoteModel();
|
||||
$this->userModel = new UserModel();
|
||||
$this->targetModel = new SalesTargetModel();
|
||||
$this->clientModel = new ClientModel();
|
||||
}
|
||||
|
||||
|
||||
@ -125,9 +128,13 @@ class SalesController extends BaseController
|
||||
->where('nhance_branch_id', $nhance_branch_id) // same branch
|
||||
->get()->getResultArray();
|
||||
|
||||
// Add "(Head)" label to heads so dropdown is clear
|
||||
// Add "(Head) (Admin)" label
|
||||
foreach ($branch_heads as &$head) {
|
||||
$head['first_name'] = $head['first_name'] . ' (Head)';
|
||||
if ($head['role'] == 1) {
|
||||
$head['first_name'] = $head['first_name'] . ' (Admin)';
|
||||
} elseif ($head['role'] == 5) {
|
||||
$head['first_name'] = $head['first_name'] . ' (Head)';
|
||||
}
|
||||
}
|
||||
unset($head);
|
||||
|
||||
@ -155,12 +162,25 @@ class SalesController extends BaseController
|
||||
->whereIn('role', [1, 5]) // Sales Head roles
|
||||
->get()->getResultArray();
|
||||
|
||||
// Add "(Head)" label to all heads
|
||||
// Add "(Head) (Admin)" label
|
||||
foreach ($all_heads as &$head) {
|
||||
$head['first_name'] = $head['first_name'] . ' (Head)';
|
||||
if ($head['role'] == 1) {
|
||||
$head['first_name'] = $head['first_name'] . ' (Admin)';
|
||||
} elseif ($head['role'] == 5) {
|
||||
$head['first_name'] = $head['first_name'] . ' (Head)';
|
||||
}
|
||||
}
|
||||
unset($head);
|
||||
|
||||
// ================================================================
|
||||
// QUERY 5: Get ALL clients
|
||||
// ================================================================
|
||||
$all_clients = $db->table('clients')
|
||||
->select('id, client_name,short_name,email,phone')
|
||||
->where('is_active', 1)
|
||||
->get()->getResultArray();
|
||||
$data['$all_clients'] = $all_clients;
|
||||
|
||||
// ================================================================
|
||||
// BUILD: sales_manager_with_head = branch heads + branch managers
|
||||
// ================================================================
|
||||
@ -272,6 +292,38 @@ class SalesController extends BaseController
|
||||
return $this->fail('Please assign this lead to a user.');
|
||||
}
|
||||
|
||||
// ── Client logic — only runs if client_id key exists in payload ──
|
||||
if (array_key_exists('client_id', $data)) {
|
||||
|
||||
$clientId = !empty($data['client_id']) ? (int)$data['client_id'] : null;
|
||||
|
||||
$clientData = array_filter([
|
||||
'client_name' => $data['company_name'] ?? null,
|
||||
'short_name' => $data['short_name'] ?? null,
|
||||
'email' => $data['email'] ?? null,
|
||||
'phone' => $data['phone'] ?? null,
|
||||
], fn($v) => $v !== null && $v !== '');
|
||||
|
||||
if ($clientId) {
|
||||
// Existing client — update name/short_name/email/phone if provided
|
||||
if (!empty($clientData)) {
|
||||
$clientData['updated_by'] = $this->getUserId();
|
||||
$this->clientModel->update($clientId, $clientData);
|
||||
}
|
||||
} else {
|
||||
// New client — insert and get ID
|
||||
$clientData['created_by'] = $this->getUserId();
|
||||
$clientData['is_active'] = 1;
|
||||
$this->clientModel->insert($clientData);
|
||||
$clientId = $this->clientModel->getInsertID();
|
||||
}
|
||||
|
||||
$data['client_id'] = $clientId;
|
||||
}
|
||||
|
||||
// ── Clean up UI-only fields before inserting lead ────────────────
|
||||
unset($data['short_name']);
|
||||
|
||||
if (!$this->leadModel->insert($data)) {
|
||||
return $this->fail($this->leadModel->errors());
|
||||
}
|
||||
@ -351,6 +403,38 @@ class SalesController extends BaseController
|
||||
$data = $this->request->getJSON(true);
|
||||
$data['updated_by'] = $this->getUserId();
|
||||
|
||||
// ── Client logic — only runs if client_id key exists in payload ──
|
||||
if (array_key_exists('client_id', $data)) {
|
||||
|
||||
$clientId = !empty($data['client_id']) ? (int)$data['client_id'] : null;
|
||||
|
||||
$clientData = array_filter([
|
||||
'client_name' => $data['company_name'] ?? null,
|
||||
'short_name' => $data['short_name'] ?? null,
|
||||
'email' => $data['email'] ?? null,
|
||||
'phone' => $data['phone'] ?? null,
|
||||
], fn($v) => $v !== null && $v !== '');
|
||||
|
||||
if ($clientId) {
|
||||
// Existing client — update if we have data
|
||||
if (!empty($clientData)) {
|
||||
$clientData['updated_by'] = $this->getUserId();
|
||||
$this->clientModel->update($clientId, $clientData);
|
||||
}
|
||||
} else {
|
||||
// New client — insert and get ID
|
||||
$clientData['created_by'] = $this->getUserId();
|
||||
$clientData['is_active'] = 1;
|
||||
$this->clientModel->insert($clientData);
|
||||
$clientId = $this->clientModel->getInsertID();
|
||||
}
|
||||
|
||||
$data['client_id'] = $clientId;
|
||||
}
|
||||
|
||||
// ── Clean up UI-only fields before inserting lead ────────────────
|
||||
unset($data['short_name']);
|
||||
|
||||
if (!$this->leadModel->update($id, $data)) {
|
||||
return $this->fail($this->leadModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
|
||||
}
|
||||
@ -408,6 +492,87 @@ class SalesController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /sales/searchClients?q=tech
|
||||
*/
|
||||
public function searchClients()
|
||||
{
|
||||
$q = trim($this->request->getGet('q') ?? '');
|
||||
|
||||
if (strlen($q) < 1) {
|
||||
return $this->response
|
||||
->setContentType('application/json')
|
||||
->setBody(json_encode(['data' => []]));
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$results = $db->table('clients')
|
||||
->select('id, client_name, short_name, email, phone')
|
||||
->like('client_name', $q)
|
||||
->where('client_type', 1)
|
||||
->where('is_active', 1)
|
||||
->limit(10)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/json')
|
||||
->setBody(json_encode(['data' => $results]));
|
||||
}
|
||||
/**
|
||||
* GET /sales/checkDuplicate?table=clients&field=short_name&value=TECH
|
||||
*/
|
||||
public function checkDuplicate()
|
||||
{
|
||||
if ($this->request->getMethod() !== 'get') {
|
||||
return $this->response
|
||||
->setStatusCode(405)
|
||||
->setContentType('application/json')
|
||||
->setBody(json_encode(['exists' => false]));
|
||||
}
|
||||
|
||||
$table = $this->request->getGet('table');
|
||||
$field = $this->request->getGet('field');
|
||||
$value = trim($this->request->getGet('value') ?? '');
|
||||
$exclude_id = $this->request->getGet('exclude_id');
|
||||
|
||||
// ── Whitelist ─────────────────────────────────────────────
|
||||
$allowed = [
|
||||
'clients' => ['model' => $this->clientModel, 'pk' => 'id'],
|
||||
];
|
||||
|
||||
// ── Allowed fields per table ──────────────────────────────
|
||||
if (!array_key_exists($table, $allowed) || !in_array($field, ['short_name', 'client_name'])) {
|
||||
return $this->response
|
||||
->setStatusCode(400)
|
||||
->setContentType('application/json')
|
||||
->setBody(json_encode(['exists' => false, 'error' => 'Invalid table or field']));
|
||||
}
|
||||
|
||||
if (empty($value)) {
|
||||
return $this->response
|
||||
->setContentType('application/json')
|
||||
->setBody(json_encode(['exists' => false]));
|
||||
}
|
||||
|
||||
$model = $allowed[$table]['model'];
|
||||
$pk = $allowed[$table]['pk'];
|
||||
|
||||
// ── Case-insensitive match ────────────────────────────────
|
||||
$model->where("LOWER({$field})", strtolower($value));
|
||||
|
||||
if (!empty($exclude_id)) {
|
||||
$model->where("{$pk} !=", (int)$exclude_id);
|
||||
}
|
||||
|
||||
$count = $model->countAllResults();
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/json')
|
||||
->setBody(json_encode(['exists' => $count > 0]));
|
||||
}
|
||||
|
||||
// ==================== CONTACT PERSON APIs ====================
|
||||
|
||||
/**
|
||||
@ -559,6 +724,7 @@ class SalesController extends BaseController
|
||||
'assigned_to' => $this->request->getGet('assigned_to'),
|
||||
'date_from' => $this->request->getGet('date_from'),
|
||||
'date_to' => $this->request->getGet('date_to'),
|
||||
'search' => $this->request->getGet('search'), // ← ADD THIS
|
||||
];
|
||||
|
||||
$result = $this->activityModel->getActivitiesWithFilters($filters, $limit, $offset);
|
||||
@ -1150,21 +1316,21 @@ public function branchLevelDashboard($branchId, $sales_manager_ids, $current_fin
|
||||
|
||||
// 1. Lead count — FY filtered by created_at
|
||||
$total_leads = $this->leadModel
|
||||
->whereIn('assigned_to', $sales_manager_ids)
|
||||
// ->whereIn('assigned_to', $sales_manager_ids)
|
||||
->where('created_at >=', $fyStart)
|
||||
->where('created_at <=', $fyEnd)
|
||||
->countAllResults();
|
||||
|
||||
// 2. Total activities — FY filtered by scheduled_date
|
||||
$total_activity = $this->activityModel
|
||||
->whereIn('assigned_to', $sales_manager_ids)
|
||||
// ->whereIn('assigned_to', $sales_manager_ids)
|
||||
->where('scheduled_date >=', $fyStart)
|
||||
->where('scheduled_date <=', $fyEnd)
|
||||
->countAllResults();
|
||||
|
||||
// 3. Completed activities — FY filtered
|
||||
$total_completed_activity = $this->activityModel
|
||||
->whereIn('assigned_to', $sales_manager_ids)
|
||||
// ->whereIn('assigned_to', $sales_manager_ids)
|
||||
->where('status', 'completed')
|
||||
->where('scheduled_date >=', $fyStart)
|
||||
->where('scheduled_date <=', $fyEnd)
|
||||
@ -1172,7 +1338,7 @@ public function branchLevelDashboard($branchId, $sales_manager_ids, $current_fin
|
||||
|
||||
// 4. Pending activities — FY filtered
|
||||
$total_pending_activity = $this->activityModel
|
||||
->whereIn('assigned_to', $sales_manager_ids)
|
||||
// ->whereIn('assigned_to', $sales_manager_ids)
|
||||
->where('status', 'pending')
|
||||
->where('scheduled_date >=', $fyStart)
|
||||
->where('scheduled_date <=', $fyEnd)
|
||||
@ -1191,7 +1357,7 @@ public function branchLevelDashboard($branchId, $sales_manager_ids, $current_fin
|
||||
AND scheduled_date <= '{$fyEnd}') as done_acts", false)
|
||||
->join('roles r', 'r.id = u.role')
|
||||
->where('u.nhance_branch_id', $branchId)
|
||||
->whereIn('u.id', $sales_manager_ids)
|
||||
// ->whereIn('u.id', $sales_manager_ids)
|
||||
->where('u.is_active', 1)
|
||||
->get()
|
||||
->getResultArray();
|
||||
@ -1203,7 +1369,7 @@ public function branchLevelDashboard($branchId, $sales_manager_ids, $current_fin
|
||||
up.first_name AS assigned_to_name, sa.notes')
|
||||
->join('user_profiles up', 'up.id = sa.assigned_to', 'left')
|
||||
->join('sales_actual_leads sal', 'sal.lead_id = sa.lead_id', 'left')
|
||||
->whereIn('sa.assigned_to', $sales_manager_ids)
|
||||
// ->whereIn('sa.assigned_to', $sales_manager_ids)
|
||||
->where('sa.status', 'pending')
|
||||
->where('sa.scheduled_date >=', $fyStart)
|
||||
->where('sa.scheduled_date <=', $fyEnd)
|
||||
@ -1223,7 +1389,7 @@ public function branchLevelDashboard($branchId, $sales_manager_ids, $current_fin
|
||||
->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')
|
||||
->whereIn('sal.assigned_to', $sales_manager_ids)
|
||||
// ->whereIn('sal.assigned_to', $sales_manager_ids)
|
||||
->where('sal.created_at >=', $fyStart)
|
||||
->where('sal.created_at <=', $fyEnd)
|
||||
->groupBy('sal.lead_id, sal.company_name, sal.status, up.first_name')
|
||||
@ -1239,7 +1405,7 @@ public function branchLevelDashboard($branchId, $sales_manager_ids, $current_fin
|
||||
->select("activity_type,
|
||||
COUNT(*) AS total,
|
||||
ROUND(COUNT(*) * 100.0 / {$total_activity_safe}, 0) AS percentage", false)
|
||||
->whereIn('assigned_to', $sales_manager_ids)
|
||||
// ->whereIn('assigned_to', $sales_manager_ids)
|
||||
->where('scheduled_date >=', $fyStart)
|
||||
->where('scheduled_date <=', $fyEnd)
|
||||
->groupBy('activity_type')
|
||||
|
||||
@ -65,13 +65,25 @@ class SalesActivityModel extends Model
|
||||
*/
|
||||
public function getActivitiesByLead($leadId, $status = null)
|
||||
{
|
||||
// Convert the INT id to a string, then wrap it in JSON quotes to match ["5", "11"]
|
||||
$subQuery = "(SELECT GROUP_CONCAT(up2.first_name SEPARATOR ', ')
|
||||
FROM user_profiles up2
|
||||
WHERE JSON_CONTAINS(sales_activities.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR)))
|
||||
) as additional_assigned_names";
|
||||
$subQuery = "(
|
||||
SELECT GROUP_CONCAT(up2.first_name SEPARATOR ', ')
|
||||
FROM user_profiles up2
|
||||
WHERE 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))
|
||||
)
|
||||
) as additional_assigned_names";
|
||||
|
||||
$builder = $this->select("sales_activities.*, up1.first_name as assigned_to_name, $subQuery")
|
||||
$builder = $this->db->table('sales_activities')
|
||||
->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);
|
||||
|
||||
@ -79,7 +91,10 @@ class SalesActivityModel extends Model
|
||||
$builder->where('sales_activities.status', $status);
|
||||
}
|
||||
|
||||
return $builder->orderBy('sales_activities.scheduled_date', 'DESC')->findAll();
|
||||
return $builder
|
||||
->orderBy('sales_activities.scheduled_date', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -182,10 +197,14 @@ class SalesActivityModel extends Model
|
||||
$builder->whereIn('sales_activities.assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
if (!empty($filters['search'])) {
|
||||
$builder->groupStart()
|
||||
if (!empty($filters['search'])) {
|
||||
// $builder->groupStart()
|
||||
$this->groupStart()
|
||||
->like('sales_actual_leads.company_name', $filters['search'])
|
||||
->orLike('sales_activities.status', $filters['search'])
|
||||
->orLike('sales_activities.activity_type', $filters['search'])
|
||||
->orLike('up1.first_name', $filters['search'])
|
||||
->orLike('up2.first_name', $filters['search'])
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ class SalesActualLeadModel extends Model
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'company_name',
|
||||
'client_id',
|
||||
'email',
|
||||
'phone',
|
||||
'address',
|
||||
@ -37,7 +38,7 @@ class SalesActualLeadModel extends Model
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [
|
||||
'company_name' => 'required|alpha_space|min_length[2]|max_length[255]',
|
||||
'company_name' => 'required|regex_match[/^[a-zA-Z0-9\s_-]+$/]|min_length[2]|max_length[255]',
|
||||
'email' => 'required_without[phone]|permit_empty|valid_email|max_length[255]',
|
||||
'phone' => 'required_without[email]|permit_empty|regex_match[/^[0-9+\s]+$/]|min_length[10]|max_length[20]',
|
||||
'status' => 'in_list[New,Potential,Prospects,Not a Prospects]',
|
||||
@ -48,7 +49,8 @@ class SalesActualLeadModel extends Model
|
||||
protected $validationMessages = [
|
||||
'company_name' => [
|
||||
'required' => 'Company Name is Missing',
|
||||
'alpha_space' => 'Company Name must contain only letters and spaces',
|
||||
// 'alpha_space' => 'Company Name must contain only letters and spaces',
|
||||
'regex_match' => 'Company Name can only contain letters, numbers, spaces, hyphens and underscores.',
|
||||
'min_length' => 'Company Name must be at least 2 characters long',
|
||||
'max_length' => 'Company Name must be at most 255 characters long'
|
||||
],
|
||||
@ -59,7 +61,7 @@ class SalesActualLeadModel extends Model
|
||||
],
|
||||
'phone' => [
|
||||
'required_without' => 'Either Email or Mobile Number is Needed.',
|
||||
'regex_match' => 'Phone number can contain only digits, + and spaces.',
|
||||
'regex_match' => 'Mobile number can contain only digits, + and spaces.',
|
||||
'min_length' => 'Mobile Number must be at least 10 characters long',
|
||||
'max_length' => 'Mobile Number must be at most 20 characters long',
|
||||
],
|
||||
@ -77,8 +79,9 @@ class SalesActualLeadModel extends Model
|
||||
*/
|
||||
public function getLeadWithUser($leadId)
|
||||
{
|
||||
return $this->select('sales_actual_leads.*, user_profiles.first_name as assigned_to_name, user_profiles.email as assigned_to_email')
|
||||
return $this->select('sales_actual_leads.*, clients.short_name, user_profiles.first_name as assigned_to_name, user_profiles.email as assigned_to_email')
|
||||
->join('user_profiles', 'user_profiles.id = sales_actual_leads.assigned_to', 'left')
|
||||
->join('clients', 'clients.id = sales_actual_leads.client_id', 'left')
|
||||
->where('sales_actual_leads.lead_id', $leadId)
|
||||
->first();
|
||||
}
|
||||
@ -88,8 +91,9 @@ class SalesActualLeadModel extends Model
|
||||
*/
|
||||
public function getLeadsWithFilters($filters = [], $limit = 10, $offset = 0)
|
||||
{
|
||||
$this->select('sales_actual_leads.*, user_profiles.first_name as assigned_to_name, user_profiles.email as assigned_to_email')
|
||||
->join('user_profiles', 'user_profiles.id = sales_actual_leads.assigned_to', 'left');
|
||||
$this->select('sales_actual_leads.*,clients.short_name, user_profiles.first_name as assigned_to_name, user_profiles.email as assigned_to_email')
|
||||
->join('user_profiles', 'user_profiles.id = sales_actual_leads.assigned_to', 'left')
|
||||
->join('clients', 'clients.id = sales_actual_leads.client_id', 'left');
|
||||
|
||||
if (!empty($filters['status'])) {
|
||||
$this->where('sales_actual_leads.status', $filters['status']);
|
||||
|
||||
@ -2469,14 +2469,24 @@
|
||||
$('#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');
|
||||
|
||||
existingShortName = actual_lead_client_details.short_name || '';
|
||||
if (existingShortName) {
|
||||
// ── Short name EXISTS — just populate and validate ────────
|
||||
$('#client_short_name').val(existingShortName);
|
||||
// validateInput($('#client_short_name')[0], 'clients', 'short_name');
|
||||
} else {
|
||||
// Run duplicate validation once
|
||||
validateInput($('#client_short_name')[0], "clients", "short_name");
|
||||
// Auto-generate short name from company name
|
||||
// Use a small delay to ensure DOM is ready
|
||||
setTimeout(function () {
|
||||
let baseName = actual_lead_client_details.company_name
|
||||
.trim()
|
||||
.substring(0, 10)
|
||||
.replace(/\s+/g, '')
|
||||
.toUpperCase();
|
||||
|
||||
makeUniqueShortName(baseName);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1284,14 +1284,24 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" data-toggle="toolt
|
||||
$('#client_name').val(actual_lead_client_details.company_name || '');
|
||||
|
||||
$('#gst').val(actual_lead_client_details.gst_number || '');
|
||||
existingShortName = actual_lead_client_details.short_name || '';
|
||||
if (existingShortName) {
|
||||
// ── Short name EXISTS — just populate and validate ────────
|
||||
$('#client_short_name').val(existingShortName);
|
||||
// validateInput($('#client_short_name')[0], 'clients', 'short_name');
|
||||
|
||||
// 🔥 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");
|
||||
// Auto-generate short name from company name
|
||||
// Use a small delay to ensure DOM is ready
|
||||
setTimeout(function () {
|
||||
let baseName = actual_lead_client_details.company_name
|
||||
.trim()
|
||||
.substring(0, 10)
|
||||
.replace(/\s+/g, '')
|
||||
.toUpperCase();
|
||||
|
||||
makeUniqueShortName(baseName);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -171,7 +171,7 @@
|
||||
</style>
|
||||
|
||||
<div class="main-content">
|
||||
<hr class="my-0">
|
||||
<hr style="margin-bottom: 0 !important;">
|
||||
<div class="lead-header">
|
||||
|
||||
<!-- LEFT SIDE -->
|
||||
@ -213,7 +213,7 @@
|
||||
<div class="modal-content" style="max-width: 850px;">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 id="det_company">Lead Detail</h2>
|
||||
<h4 class="modal-title" id="det_company" style="margin-top: 5px;" >Lead Detail</h2>
|
||||
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
||||
</div>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('leadDetailModal')">×</button>
|
||||
@ -408,7 +408,7 @@
|
||||
<div class="modal" id="opportunityModal">
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Select Opportunity Type</h2>
|
||||
<h4 class="modal-title" style="margin-top: 15px;margin-bottom: 15px;">Select Opportunity Type</h4>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('opportunityModal')">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
@ -683,7 +683,7 @@ async function fetchActivities(isLoadMore = false) {
|
||||
${activityIcons[a.activity_type]} ${a.activity_type}
|
||||
</span>
|
||||
<span class="activity-status-badge status-${a.status.toLowerCase().replace(' ', '-')}"">
|
||||
${a.status}
|
||||
${capitalize(a.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="activity-details">
|
||||
@ -940,6 +940,11 @@ function openOpportunityModal() {
|
||||
openModal('opportunityModal');
|
||||
}
|
||||
|
||||
function capitalize(str) {
|
||||
if (!str) return '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
function switchTab(tabName) {
|
||||
//Default Activity Tab how to resset here
|
||||
// 1. Hide all tab content
|
||||
|
||||
@ -145,7 +145,7 @@
|
||||
<div class="tab" data-filter="Prospects" onclick="setFilter('Prospects', this)">Prospects</div>
|
||||
<div class="tab" data-filter="Not a Prospects" onclick="setFilter('Not a Prospects', this)">Not a Prospects</div>
|
||||
</div> -->
|
||||
<hr class="my-0">
|
||||
<hr style="margin-bottom: 0 !important;">
|
||||
<div class="lead-header">
|
||||
|
||||
<!-- LEFT SIDE -->
|
||||
@ -196,10 +196,43 @@
|
||||
<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 class="col-xl-12 col-lg-12 col-md-12" style="position:relative;">
|
||||
|
||||
<!-- Label with inline short name badge -->
|
||||
<label class="form-label">
|
||||
Company Name<span class="text-danger">*</span>
|
||||
|
||||
<span id="shortNameBadge" style="display:none; background:#FAEEDA; color:#854F0B;
|
||||
padding:2px 10px; border-radius:999px; font-size:11px; font-weight:600;
|
||||
letter-spacing:0.5px; vertical-align:middle;">
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Hidden fields -->
|
||||
<input type="hidden" name="client_id" id="lead_client_id" value="">
|
||||
<input type="hidden" name="short_name" id="lead_short_name_hidden" value="">
|
||||
|
||||
|
||||
<!-- Selected pill (shown after picking existing client) -->
|
||||
<div id="companySelected" style="display:none; align-items:center; gap:8px;
|
||||
border:1px solid #B5D4F4; border-radius:6px; padding:6px 12px; background:#E6F1FB;">
|
||||
<span id="companySelectedName" style="flex:1; font-size:14px; font-weight:500;"></span>
|
||||
<span onclick="clearCompanySelection('add')" style="cursor:pointer; color:#888; font-size:18px; line-height:1;">×</span>
|
||||
</div>
|
||||
|
||||
<!-- Search input -->
|
||||
<input type="text" id="companySearchInput" class="form-control"
|
||||
name="company_name" placeholder="Search or create company..."
|
||||
autocomplete="off" oninput="searchCompany(this.value, 'add')" required
|
||||
style="width:100%;">
|
||||
|
||||
<!-- Dropdown -->
|
||||
<div id="companyDropdown" style="display:none; position:absolute; z-index:9999;
|
||||
width:100%; background:#fff; border:1px solid #ddd; border-radius:6px;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.1); max-height:220px; overflow-y:auto;
|
||||
top:100%; left:0;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row mb-1">
|
||||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||||
@ -207,7 +240,7 @@
|
||||
<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>
|
||||
<label class="form-label">Mobile</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, '')"> -->
|
||||
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10);">
|
||||
</div>
|
||||
@ -248,7 +281,7 @@
|
||||
<div class="modal-content" style="max-width: 850px;">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 id="det_company">Lead Detail</h2>
|
||||
<h4 class="modal-title" id="det_company" style="margin-top: 5px;" >Lead Detail</h2>
|
||||
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
||||
</div>
|
||||
<button class="btn-close" onclick="closeModal('leadDetailModal')" title="Close">×</button>
|
||||
@ -435,7 +468,7 @@
|
||||
<div class="modal" id="opportunityModal">
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Select Opportunity Type</h2>
|
||||
<h4 class="modal-title" style="margin-top: 15px;margin-bottom: 15px;">Select Opportunity Type</h4>
|
||||
<button class="btn-close" onclick="closeModal('opportunityModal')" title="Close">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
@ -470,7 +503,7 @@
|
||||
</div>
|
||||
|
||||
<div class="modal" id="editLeadModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content" style="overflow-x: hidden;">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Edit Lead</h4>
|
||||
<button class="btn-close" onclick="closeModal('editLeadModal')" title="Close">×</button>
|
||||
@ -481,10 +514,47 @@
|
||||
<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">
|
||||
<!-- <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 class="col-xl-12 col-lg-12 col-md-12" style="position:relative;">
|
||||
|
||||
<!-- Label with inline short name badge -->
|
||||
<label class="form-label">
|
||||
Company Name <span class="text-danger">*</span>
|
||||
|
||||
<span id="editShortNameBadge" style="display:none; background:#FAEEDA; color:#854F0B;
|
||||
padding:2px 10px; border-radius:999px; font-size:11px; font-weight:600;
|
||||
letter-spacing:0.5px; vertical-align:middle;">
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Hidden fields -->
|
||||
<input type="hidden" name="client_id" id="edit_lead_client_id" value="">
|
||||
<input type="hidden" name="short_name" id="edit_lead_short_name_hidden" value="">
|
||||
|
||||
<!-- Selected pill (shown after picking existing client) -->
|
||||
<div id="editCompanySelected" style="display:none; align-items:center; gap:8px;
|
||||
border:1px solid #B5D4F4; border-radius:6px; padding:6px 12px; background:#E6F1FB;">
|
||||
<span id="editCompanySelectedName" style="flex:1; font-size:14px; font-weight:500;"></span>
|
||||
<span onclick="clearCompanySelection('edit')" style="cursor:pointer; color:#888; font-size:18px; line-height:1;">×</span>
|
||||
</div>
|
||||
|
||||
<!-- Search input -->
|
||||
<input type="text" id="editCompanySearchInput" class="form-control"
|
||||
name="company_name" placeholder="Search or create company..."
|
||||
autocomplete="off" oninput="searchCompany(this.value, 'edit')" required
|
||||
style="width:100%;">
|
||||
|
||||
<!-- Dropdown -->
|
||||
<div id="editCompanyDropdown" style="display:none; position:absolute; z-index:9999;
|
||||
width:100%; background:#fff; border:1px solid #ddd; border-radius:6px;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.1); max-height:220px; overflow-y:auto;
|
||||
top:100%; left:0;">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row mb-1">
|
||||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||||
@ -492,7 +562,7 @@
|
||||
<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>
|
||||
<label class="form-label">Mobile</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, '')"> -->
|
||||
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10);">
|
||||
</div>
|
||||
@ -553,28 +623,44 @@
|
||||
<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">
|
||||
<label class="form-label">Person Name <span class="text-danger">*</span></label>
|
||||
<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">
|
||||
<label class="form-label">Mobile <span class="text-danger">*</span></label>
|
||||
<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">
|
||||
<label class="form-label">Designation </label>
|
||||
<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">
|
||||
<label class="form-label"> </label>
|
||||
<div class="primary-container">
|
||||
<label for="contact_is_primary" class="primary-label">Primary</label>
|
||||
<input class="form-check-input" type="checkbox" id="contact_is_primary">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-6">
|
||||
<!-- <div class="col-md-2 col-6">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" id="btnSaveContact" class="btn btn-sm btn-info text-white" style="width:85%">
|
||||
✔ Save
|
||||
</button>
|
||||
</div> -->
|
||||
<div class="col-md-2 col-6">
|
||||
<label class="form-label"> </label>
|
||||
<div style="display:flex; gap:2px;">
|
||||
<button type="button" id="btnSaveContact" class="btn btn-sm btn-info text-white" style="width:42%" title="Save">
|
||||
✔
|
||||
</button>
|
||||
<button type="button" id="btnClearContact" class="btn btn-sm btn-secondary text-white" style="width:42%" title="Clear">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -713,13 +799,22 @@ function closeModal(id) {
|
||||
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';
|
||||
// }
|
||||
// Replace both add and edit company cleanup blocks with:
|
||||
if (id === 'addLeadModal') {
|
||||
_resetCompanyUI('add');
|
||||
}
|
||||
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.querySelectorAll('[name="status"] option').forEach(opt => opt.hidden = false);
|
||||
form.querySelector('[name="status"]').value = 'New';
|
||||
_resetCompanyUI('edit');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -730,6 +825,32 @@ function selectType(val, el) {
|
||||
selectedType = val;
|
||||
}
|
||||
|
||||
function resetContactForm() {
|
||||
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;
|
||||
document.getElementById('btnSaveContact').innerHTML = "✔";
|
||||
document.getElementById('btnSaveContact').title = "Save";
|
||||
document.getElementById('btnSaveContact').style.background = "#02a8b5";
|
||||
}
|
||||
// Helper — add this once anywhere in your JS
|
||||
function _resetCompanyUI(mode) {
|
||||
const ctx = COMPANY_CTX[mode];
|
||||
const state = companyState[mode];
|
||||
state.isExisting = false;
|
||||
state.selectedValue = '';
|
||||
document.getElementById(ctx.clientId).value = '';
|
||||
document.getElementById(ctx.shortNameHidden).value = ''; // ← only this
|
||||
document.getElementById(ctx.selected).style.display = 'none';
|
||||
document.getElementById(ctx.searchInput).style.display = '';
|
||||
document.getElementById(ctx.searchInput).value = '';
|
||||
document.getElementById(ctx.dropdown).style.display = 'none';
|
||||
const b = document.getElementById(ctx.badge);
|
||||
if (b) { b.textContent = ''; b.style.display = 'none'; }
|
||||
}
|
||||
|
||||
|
||||
function selectFollowUpActivityType(val, el) {
|
||||
document.querySelectorAll('.f_activity_type').forEach(b => b.classList.remove('active'));
|
||||
@ -1143,6 +1264,10 @@ document.getElementById('addLeadForm').onsubmit = async (e) => {
|
||||
// let phoneRegex = /^\+?[0-9\s]{0,10}$/; // Phone regex (+, numbers, spaces allowed, 10-20 length)
|
||||
let phoneRegex = /^\d{10}$/;
|
||||
|
||||
let addState = companyState['add']; // use 'edit' in editLeadForm
|
||||
let modeState = companyState['add']; // ← change to 'edit' for edit form
|
||||
|
||||
|
||||
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
|
||||
function showError(message, fieldName) {
|
||||
toastr.warning(message, 'Validation Error');
|
||||
@ -1151,6 +1276,13 @@ document.getElementById('addLeadForm').onsubmit = async (e) => {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
if (!modeState.selectedValue && !data.client_id) {
|
||||
return showError('Please select a company from the dropdown or click "Create" to add a new one.', 'company_name');
|
||||
}
|
||||
if (!companyState['add'].selectedValue) {
|
||||
return showError('Please select or create a company from the dropdown.', 'company_name');
|
||||
}
|
||||
|
||||
// --- VALIDATION CHECKS (Button is still normal here) ---
|
||||
if (!companyName && !email && !phone && !data.assigned_to) {
|
||||
toastr.warning('Please fill in all the required fields.');
|
||||
@ -1256,6 +1388,9 @@ document.getElementById('editLeadForm').onsubmit = async (e) => {
|
||||
// Just grab the button variables first, DO NOT disable yet
|
||||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.innerText;
|
||||
let addState = companyState['edit']; // use 'edit' in editLeadForm
|
||||
let modeState = companyState['edit']; // ← change to 'edit' for edit form
|
||||
|
||||
|
||||
const data = Object.fromEntries(new FormData(e.target).entries());
|
||||
console.log("Lead :", data);
|
||||
@ -1281,6 +1416,14 @@ document.getElementById('editLeadForm').onsubmit = async (e) => {
|
||||
}
|
||||
|
||||
// --- VALIDATION CHECKS (Button is still normal here) ---
|
||||
if (!modeState.selectedValue && !data.client_id) {
|
||||
return showError('Please select a company from the dropdown or click "Create" to add a new one.', 'company_name');
|
||||
}
|
||||
|
||||
if (!companyState['edit'].selectedValue) {
|
||||
return showError('Please select or create a company from the dropdown.', 'company_name');
|
||||
}
|
||||
|
||||
if (!companyName && !email && !phone && !data.assigned_to) {
|
||||
toastr.warning('Please fill in all the required fields.');
|
||||
return;
|
||||
@ -1343,7 +1486,9 @@ document.getElementById('editLeadForm').onsubmit = async (e) => {
|
||||
$('#contactPersonsList').find('.contact-card').remove();
|
||||
$('#noContactPersonsData').show();
|
||||
document.getElementById('editing_contact_id').value = '';
|
||||
document.getElementById('btnSaveContact').innerHTML = "✔ Save";
|
||||
document.getElementById('btnSaveContact').innerHTML = "✔";
|
||||
document.getElementById('btnSaveContact').title = "Save";
|
||||
document.getElementById('btnSaveContact').style.background = "#02a8b5";
|
||||
} else {
|
||||
let err = await res.json();
|
||||
if (res.status === 400) {
|
||||
@ -1688,8 +1833,11 @@ document.getElementById('do_follow').addEventListener('change', function () {
|
||||
|
||||
|
||||
async function openEditLeadModal(id) {
|
||||
|
||||
// 1. Reset UI State
|
||||
document.getElementById('hidden_lead_id').value = id;
|
||||
// ✅ ADD THIS — reset contact form state every time modal opens
|
||||
resetContactForm();
|
||||
const container = $('#contactPersonsList');
|
||||
// Remove only previous contact cards, keep the NoData span for now
|
||||
container.find('.contact-card').remove();
|
||||
@ -1706,6 +1854,22 @@ async function openEditLeadModal(id) {
|
||||
|
||||
// 3. Populate Form Fields
|
||||
const form = document.getElementById('editLeadForm');
|
||||
// Populate company for edit modal
|
||||
_resetCompanyUI('edit');
|
||||
if (lead.client_id) {
|
||||
selectExistingCompany({
|
||||
id: lead.client_id,
|
||||
client_name: lead.company_name || '',
|
||||
short_name: lead.short_name || '',
|
||||
email: '', // don't overwrite existing email
|
||||
phone: '', // don't overwrite existing phone
|
||||
}, 'edit');
|
||||
} else {
|
||||
// No linked client — just show the typed name, allow search
|
||||
companyState['edit'].selectedValue = lead.company_name || '';
|
||||
const inp = document.getElementById('editCompanySearchInput');
|
||||
inp.value = lead.company_name || '';
|
||||
}
|
||||
|
||||
// Mapping fields carefully
|
||||
form.querySelector('[name="company_name"]').value = lead.company_name || '';
|
||||
@ -1786,9 +1950,9 @@ document.getElementById('contactPersonsList').onclick = async (e) => {
|
||||
document.getElementById('contact_is_primary').checked = contactData.is_primary == 1;
|
||||
|
||||
// Change Button UI
|
||||
const saveBtn = document.getElementById('btnSaveContact');
|
||||
saveBtn.innerHTML = "Update";
|
||||
|
||||
document.getElementById('btnSaveContact').innerHTML = "✏️";
|
||||
document.getElementById('btnSaveContact').title = "Update";
|
||||
document.getElementById('btnSaveContact').style.background = "#02a8b5";
|
||||
document.getElementById('contact_name').focus();
|
||||
}
|
||||
|
||||
@ -1841,14 +2005,8 @@ if (btnSaveContact) {
|
||||
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";
|
||||
resetContactForm();
|
||||
|
||||
// Refresh List
|
||||
openEditLeadModal(leadId);
|
||||
} else {
|
||||
@ -1857,7 +2015,11 @@ if (btnSaveContact) {
|
||||
}
|
||||
} catch (error) { console.error(error); }
|
||||
};
|
||||
}
|
||||
}
|
||||
const btnClearContact = document.getElementById('btnClearContact');
|
||||
if (btnClearContact) {
|
||||
btnClearContact.onclick = () => resetContactForm();
|
||||
}
|
||||
|
||||
|
||||
const statusOrder = ['New', 'Potential', 'Prospects', 'Not a Prospects'];
|
||||
@ -1887,6 +2049,7 @@ if (btnSaveContact) {
|
||||
|
||||
|
||||
|
||||
|
||||
function convertDBFormatted(input) {
|
||||
|
||||
if (!input) return null;
|
||||
@ -1916,4 +2079,277 @@ function convertDBFormatted(input) {
|
||||
}
|
||||
fetchLeads();
|
||||
|
||||
// ================================================================
|
||||
// COMPANY SEARCH — Odoo-style autocomplete
|
||||
// UNIFIED COMPANY SEARCH — works for both Add & Edit modals
|
||||
// ================================================================
|
||||
|
||||
// Context config for each modal
|
||||
const COMPANY_CTX = {
|
||||
add: {
|
||||
searchInput: 'companySearchInput',
|
||||
dropdown: 'companyDropdown',
|
||||
selected: 'companySelected',
|
||||
selectedName: 'companySelectedName',
|
||||
badge: 'shortNameBadge',
|
||||
clientId: 'lead_client_id',
|
||||
shortNameHidden:'lead_short_name_hidden',
|
||||
formId: 'addLeadForm',
|
||||
},
|
||||
edit: {
|
||||
searchInput: 'editCompanySearchInput',
|
||||
dropdown: 'editCompanyDropdown',
|
||||
selected: 'editCompanySelected',
|
||||
selectedName: 'editCompanySelectedName',
|
||||
badge: 'editShortNameBadge',
|
||||
clientId: 'edit_lead_client_id',
|
||||
shortNameHidden:'edit_lead_short_name_hidden',
|
||||
formId: 'editLeadForm',
|
||||
}
|
||||
};
|
||||
|
||||
// Track state per modal
|
||||
const companyState = { add: { isExisting: false, selectedValue: '' },
|
||||
edit: { isExisting: false, selectedValue: '' } };
|
||||
|
||||
// Timer per modal
|
||||
const companyTimers = { add: null, edit: null };
|
||||
|
||||
// ── Called from oninput on both search inputs ─────────────────────
|
||||
function searchCompany(val, mode) {
|
||||
const ctx = COMPANY_CTX[mode];
|
||||
const state = companyState[mode];
|
||||
clearTimeout(companyTimers[mode]);
|
||||
|
||||
const dropdown = document.getElementById(ctx.dropdown);
|
||||
const badge = document.getElementById(ctx.badge);
|
||||
|
||||
if (!val || val.trim().length < 1) {
|
||||
dropdown.style.display = 'none';
|
||||
badge.textContent = '';
|
||||
badge.style.display = 'none';
|
||||
document.getElementById(ctx.shortNameHidden).value = '';
|
||||
document.getElementById(ctx.shortNameField).value = '';
|
||||
state.selectedValue = ''; // ← clear selected value tracking
|
||||
return;
|
||||
}
|
||||
|
||||
// Only generate short name for new clients while typing
|
||||
if (!state.isExisting) {
|
||||
generateAndSetShortName(val, mode);
|
||||
}
|
||||
|
||||
companyTimers[mode] = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`<?= base_url('sales/searchClients') ?>?q=${encodeURIComponent(val)}`);
|
||||
const json = await res.json();
|
||||
renderCompanyDropdown(json.data || [], val, mode);
|
||||
} catch (e) {
|
||||
renderCompanyDropdown([], val, mode);
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function renderCompanyDropdown(results, query, mode) {
|
||||
const ctx = COMPANY_CTX[mode];
|
||||
const dropdown = document.getElementById(ctx.dropdown);
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
// ── Existing matches ──────────────────────────────────────────
|
||||
results.forEach(client => {
|
||||
const safe = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const highlighted = client.client_name.replace(
|
||||
new RegExp(`(${safe})`, 'gi'),
|
||||
'<strong style="color:#185FA5;">$1</strong>'
|
||||
);
|
||||
const initials = client.client_name.substring(0, 2).toUpperCase();
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.style.cssText = 'display:flex;align-items:center;gap:10px;padding:9px 14px;cursor:pointer;font-size:13px;border-bottom:1px solid #f5f5f5;';
|
||||
item.innerHTML = `
|
||||
<div style="width:32px;height:32px;border-radius:50%;background:#B5D4F4;display:flex;
|
||||
align-items:center;justify-content:center;font-size:11px;font-weight:600;
|
||||
color:#0C447C;flex-shrink:0;">${initials}</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:500;">${highlighted}</div>
|
||||
<div style="font-size:11px;color:#aaa;margin-top:1px;">
|
||||
<span style="background:#e8f4fd;color:#185FA5;padding:1px 6px;border-radius:4px;font-size:10px;">${client.short_name || 'N/A'}</span>
|
||||
${client.email ? ` ${client.email}` : ''}
|
||||
${client.phone ? ` · ${client.phone}` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
item.onmouseenter = () => item.style.background = '#f7f9fc';
|
||||
item.onmouseleave = () => item.style.background = '';
|
||||
item.onclick = () => selectExistingCompany(client, mode);
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
|
||||
// ── Divider ───────────────────────────────────────────────────
|
||||
if (results.length > 0) {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'height:1px;background:#eee;margin:4px 0;';
|
||||
dropdown.appendChild(div);
|
||||
}
|
||||
|
||||
// ── Create new option ─────────────────────────────────────────
|
||||
const createItem = document.createElement('div');
|
||||
createItem.style.cssText = 'display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;font-size:13px;color:#185FA5;font-weight:500;';
|
||||
createItem.innerHTML = `
|
||||
<span style="background:#E6F1FB;border-radius:4px;padding:2px 8px;font-size:11px;font-weight:700;">+</span>
|
||||
Create <strong>"${query}"</strong>`;
|
||||
createItem.onmouseenter = () => createItem.style.background = '#f0f7ff';
|
||||
createItem.onmouseleave = () => createItem.style.background = '';
|
||||
createItem.onclick = () => selectNewCompany(query, mode);
|
||||
dropdown.appendChild(createItem);
|
||||
|
||||
dropdown.style.display = 'block';
|
||||
}
|
||||
|
||||
// ── User picked an EXISTING client from dropdown ──────────────────
|
||||
function selectExistingCompany(client, mode) {
|
||||
const ctx = COMPANY_CTX[mode];
|
||||
const state = companyState[mode];
|
||||
state.isExisting = true;
|
||||
state.selectedValue = client.client_name;
|
||||
|
||||
document.getElementById(ctx.clientId).value = client.id;
|
||||
document.getElementById(ctx.shortNameHidden).value = client.short_name || ''; // ← only this
|
||||
|
||||
const searchInp = document.getElementById(ctx.searchInput);
|
||||
searchInp.value = client.client_name;
|
||||
searchInp.style.display = 'none';
|
||||
document.getElementById(ctx.selectedName).textContent = client.client_name;
|
||||
document.getElementById(ctx.selected).style.display = 'flex';
|
||||
document.getElementById(ctx.dropdown).style.display = 'none';
|
||||
|
||||
const badge = document.getElementById(ctx.badge);
|
||||
badge.textContent = client.short_name || '';
|
||||
badge.style.cssText = 'display:inline-block;background:#E6F1FB;color:#185FA5;' +
|
||||
'padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600;' +
|
||||
'letter-spacing:0.5px;vertical-align:middle;';
|
||||
|
||||
const form = document.getElementById(ctx.formId);
|
||||
const ef = form.querySelector('[name="email"]');
|
||||
const pf = form.querySelector('[name="phone"]');
|
||||
if (ef && client.email && !ef.value) ef.value = client.email;
|
||||
if (pf && client.phone && !pf.value) pf.value = client.phone;
|
||||
}
|
||||
|
||||
// ── User clicked "Create new" ─────────────────────────────────────
|
||||
async function selectNewCompany(name, mode) {
|
||||
// ── Duplicate check ──────────────────────────────────
|
||||
try {
|
||||
const res = await fetch(`<?= base_url('sales/checkDuplicate') ?>?table=clients&field=client_name&value=${encodeURIComponent(name)}`);
|
||||
const json = await res.json();
|
||||
if (json.exists) {
|
||||
toastr.warning(`"${name}" already exists. Search and select it from the dropdown instead.`);
|
||||
document.getElementById(COMPANY_CTX[mode].searchInput).value = '';
|
||||
document.getElementById(COMPANY_CTX[mode].searchInput).focus();
|
||||
return;
|
||||
}
|
||||
} catch(e) {
|
||||
// silently allow on network error — server will catch it
|
||||
}
|
||||
// ── Rest of your existing code (unchanged) ───────────
|
||||
const ctx = COMPANY_CTX[mode];
|
||||
const state = companyState[mode];
|
||||
state.isExisting = false;
|
||||
state.selectedValue = name; // ← track what was confirmed as new
|
||||
document.getElementById(ctx.clientId).value = '';
|
||||
const inp = document.getElementById(ctx.searchInput);
|
||||
inp.value = name;
|
||||
document.getElementById(ctx.dropdown).style.display = 'none';
|
||||
generateAndSetShortName(name, mode);
|
||||
|
||||
}
|
||||
|
||||
// ── × button on pill ─────────────────────────────────────────────
|
||||
function clearCompanySelection(mode) {
|
||||
const ctx = COMPANY_CTX[mode];
|
||||
const state = companyState[mode];
|
||||
state.isExisting = false;
|
||||
state.selectedValue = '';
|
||||
|
||||
document.getElementById(ctx.clientId).value = '';
|
||||
document.getElementById(ctx.shortNameHidden).value = ''; // ← only this
|
||||
document.getElementById(ctx.selected).style.display = 'none';
|
||||
|
||||
const badge = document.getElementById(ctx.badge);
|
||||
badge.textContent = '';
|
||||
badge.style.display = 'none';
|
||||
|
||||
const inp = document.getElementById(ctx.searchInput);
|
||||
inp.style.display = '';
|
||||
inp.value = '';
|
||||
inp.focus();
|
||||
|
||||
if (mode === 'add') {
|
||||
const form = document.getElementById(ctx.formId);
|
||||
const ef = form.querySelector('[name="email"]');
|
||||
const pf = form.querySelector('[name="phone"]');
|
||||
if (ef) ef.value = '';
|
||||
if (pf) pf.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Short name generation (shared) ───────────────────────────────
|
||||
function generateAndSetShortName(name, mode) {
|
||||
// FIX 2.2: trim ALL spaces before generating
|
||||
const base = name.trim().replace(/\s+/g, '').substring(0, 10).toUpperCase();
|
||||
if (!base) return;
|
||||
|
||||
checkShortNameInBothTables(base, (isDupe) => {
|
||||
if (!isDupe) {
|
||||
setShortName(base, mode);
|
||||
} else {
|
||||
let counter = 1;
|
||||
const tryNext = () => {
|
||||
const candidate = base.substring(0, 8) + String(counter).padStart(2, '0');
|
||||
checkShortNameInBothTables(candidate, (exists) => {
|
||||
if (exists) { counter++; tryNext(); }
|
||||
else { setShortName(candidate, mode); }
|
||||
});
|
||||
};
|
||||
tryNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setShortName(val, mode) {
|
||||
const ctx = COMPANY_CTX[mode];
|
||||
document.getElementById(ctx.shortNameHidden).value = val; // ← only this
|
||||
|
||||
const badge = document.getElementById(ctx.badge);
|
||||
badge.textContent = val;
|
||||
badge.style.cssText = 'display:inline-block;background:#FAEEDA;color:#854F0B;' +
|
||||
'padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600;' +
|
||||
'letter-spacing:0.5px;vertical-align:middle;';
|
||||
}
|
||||
|
||||
function checkDuplicateTableFieldValue(table, field, value, callback) {
|
||||
if (!value || value.trim() === '') { callback(false); return; }
|
||||
fetch(`<?= base_url('sales/checkDuplicate') ?>?table=${encodeURIComponent(table)}&field=${encodeURIComponent(field)}&value=${encodeURIComponent(value)}`)
|
||||
.then(res => res.json())
|
||||
.then(json => callback(json.exists === true))
|
||||
.catch(() => callback(false));
|
||||
}
|
||||
|
||||
function checkShortNameInBothTables(value, callback) {
|
||||
let results = { clients: false, leads: false };
|
||||
let completed = 0;
|
||||
const done = () => { completed++; if (completed === 2) callback(results.clients || results.leads); };
|
||||
checkDuplicateTableFieldValue('clients', 'short_name', value, (d) => { results.clients = d; done(); });
|
||||
}
|
||||
|
||||
// ── Close dropdowns on outside click ─────────────────────────────
|
||||
document.addEventListener('click', (e) => {
|
||||
['add', 'edit'].forEach(mode => {
|
||||
const ctx = COMPANY_CTX[mode];
|
||||
if (!e.target.closest('#' + ctx.searchInput) && !e.target.closest('#' + ctx.dropdown)) {
|
||||
const dd = document.getElementById(ctx.dropdown);
|
||||
if (dd) dd.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user