nhance/app/Views/sales/tracker_view.php

2842 lines
138 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<style>
/* POC Exact Styling */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* .top-bar { background: white; padding: 15px 30px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } */
.btn-primary { background: #02a8b5; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary:hover { background: #02a8b5; transform: translateY(-1px); }
.btn-close { background: none; border: none; font-size: 22px; cursor: pointer; color: #888; width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: background .2s; }
.btn-close:hover { background: #f0f0f0; color: #333; }
/* Filter Tabs */
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
.tab { padding: 10px 14px; border-radius: 20px; cursor: pointer; font-size: 12px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; white-space: nowrap; flex: 0 0 auto; }
.tab.active { background: #02a8b5; color: white; border-color: #02a8b5; }
/* Leads Grid */
.lead-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; /* responsive */ gap: 15px; }
.lead-actions { display: flex; align-items: center; gap: 4px; }
.sales-toolbar { flex-wrap: nowrap; padding: 16px 30px; gap: 12px; overflow-x: auto; }
.sales-toolbar .filter-tabs { padding: 0; flex: 1 1 auto; min-width: 0; overflow-x: auto; }
.sales-toolbar .lead-actions { flex: 0 0 auto; flex-wrap: nowrap; }
.sales-toolbar .search-input { width: 230px !important; }
.sales-toolbar .toolbar-select { width: 175px !important; min-width: 175px; padding: 9px 12px; }
.sales-toolbar .toolbar-select + .select2-container { width: 175px !important; min-width: 175px; }
.sales-toolbar .financial-year-select { width: 108px !important; min-width: 108px; }
.sales-toolbar .select2-container .select2-selection--single { height: 38px; border: 1px solid #ddd; border-radius: 6px; display: flex; align-items: center; }
.sales-toolbar .select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 36px; }
.sales-toolbar .select2-container--default .select2-selection--single .select2-selection__arrow { height: 36px; }
.sales-toolbar #memberFilter + .select2-container { width: 175px !important; min-width: 175px; flex: 0 0 175px; }
.sales-toolbar #memberFilter + .select2-container .select2-selection--single { height: 38px !important; background: #fff !important; border: 1px solid #ddd !important; border-radius: 6px !important; display: flex; align-items: center; }
.sales-toolbar #memberFilter + .select2-container .select2-selection__rendered { flex: 1; line-height: 36px !important; padding-left: 12px; padding-right: 28px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sales-toolbar #memberFilter + .select2-container .select2-selection__arrow { height: 25px !important; width: 20px; top: 1px; right: 6px; }
.toolbar-icon-btn { width: 38px; height: 38px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 8px; color: #fff; cursor: pointer; transition: all 0.2s; }
.toolbar-icon-btn:hover { transform: translateY(-1px); filter: brightness(0.96); }
.toolbar-icon-btn:focus { outline: none; box-shadow: 0 0 0 2px rgba(2, 168, 181, 0.25); }
.toolbar-icon-btn i { font-size: 20px; line-height: 1; }
.toolbar-icon-btn.export-btn { background: #1f9d55; }
.toolbar-icon-btn.add-btn { background: #02a8b5; }
.leads-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 0 30px 30px; overflow-y: auto; }
.lead-card { background: white; border-radius: 12px; padding: 20px; border: 1px solid #e0e0e0; cursor: pointer; transition: all 0.2s; }
.lead-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); }
.lead-status { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; margin-top: 5px; }
.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; }
.status-unknown { background: #000; color: #fff; }
.status-text-unknown { color: #000; font-weight: bold; }
.status-text-lost { color: #d32f2f; font-weight: bold; }
.status-text-won { color: #388e3c; font-weight: bold; }
/* Modals */
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center; }
.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; }
/* Activity Type Buttons (POC Style) */
.activity-types { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px; }
.activity-type-btn { padding: 12px; border: 1px solid #e0e0e0; background: white; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 14px; transition: 0.2s; }
.activity-type-btn:hover { border-color: #ff6b35; background: #fff5f2; }
.activity-type-btn.active { border-color: #ff6b35; background: #ff6b35; color: white; }
.activity-types { display: grid; grid-template-columns: repeat(12, 1fr); /* 12-column grid */ gap: 10px; margin-bottom: 20px; }
/* First 3 buttons → 4 columns each (3 × 4 = 12) */
.activity-types button:nth-child(-n+3) { grid-column: span 4; }
/* Next 4 buttons → 3 columns each (4 × 3 = 12) */
.activity-types button:nth-child(n+4) { grid-column: span 3; }
/* Timeline Styling */
.timeline { position: relative; padding-left: 30px; margin-top: 20px; }
.timeline-item { position: relative; padding-bottom: 25px; }
.timeline-item::before { content: ''; position: absolute; left: -21px; top: 10px; width: 2px; height: 100%; background: #e0e0e0; }
.timeline-dot { position: absolute; left: -26px; top: 2px; width: 12px; height: 12px; border-radius: 50%; background: #ff6b35; border: 2px solid white; box-shadow: 0 0 0 1px #ff6b35; }
.timeline-dot.completed { background: #4caf50; box-shadow: 0 0 0 1px #4caf50; }
.timeline-content { background: #f8f8f8; padding: 15px; border-radius: 8px; }
.timeline-item {display : block !important;}
.timeline::before { display: none !important; content: none !important; }
.timeline.no-line::before { display: none; }
.timeline { border-left: none !important; }
.timeline-item:last-child { border-left: none !important;}
.timeline-item:last-child::before { display: none !important;}
/* opportunities Styling */
.opportunities-list { display: grid; gap: 15px; margin-bottom: 12px; }
.opportunity-card { background: white; border-radius: 12px; padding: 20px; border: 1px solid #e0e0e0; transition: all 0.2s;}
.opportunity-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 15px;}
.opportunity-title { font-weight: 600; font-size: 16px; margin-bottom: 5px;}
.opportunity-amount { font-size: 20px; font-weight: 700; color: #4caf50;}
.opportunity-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;}
.opportunity-detail-item { font-size: 13px; color: #666;}
.opportunity-footer { margin-top: 15px; padding-top: 15px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #666; }
.lost-reason { margin-bottom: 8px; color: #c0392b; /* soft red for lost */ }
.footer-divider { border-top: 1px dashed #ddd; margin: 8px 0; }
.notes { color: #555; }
/* Base style for both tabs */
.tab-item { cursor: pointer; padding-bottom: 10px; margin: 0; font-size: 16px; color: #999; /* Default grey for unselected */ border-bottom: 2px solid transparent; transition: all 0.2s ease; }
/* Style for the SELECTED tab */
.tab-item.active { color: #333; /* Darker text */ border-bottom: 2px solid #ff6b35; /* Orange underline */ }
.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;
}
/* The animation definition */
@keyframes validation-blink {
0% { transform: translateX(0); box-shadow: 0 0 0px red; }
25% { transform: translateX(-5px); box-shadow: 0 0 10px red; }
50% { transform: translateX(5px); box-shadow: 0 0 10px red; }
75% { transform: translateX(-5px); box-shadow: 0 0 10px red; }
100% { transform: translateX(0); box-shadow: 0 0 0px red; }
}
/* The class we will add via JavaScript */
.blink-error {
animation: validation-blink 0.3s ease-in-out;
border-color: #ff4d4d !important;
}
.select2-container--default
.select2-selection--multiple
.select2-selection__choice {
background-color: #02a8b5 !important;
border: none !important;
border-color: #fff !important;
color: #fff !important;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: #fff !important;
}
.modal .select2-container--default .select2-selection--multiple {
background-color: #fff !important;
}
</style>
<div class="main-content">
<!-- <div class="top-bar">
<input type="text" class="search-input" id="mainSearch" placeholder="Search leads..." onkeyup="fetchLeads()">
<button class="btn-primary" onclick="openModal('addLeadModal')">+ Add Lead</button>
</div>
<div class="filter-tabs">
<div class="tab active" data-filter="all" onclick="setFilter('all', this)">All</div>
<div class="tab" data-filter="New" onclick="setFilter('New', this)">New</div>
<div class="tab" data-filter="Potential" onclick="setFilter('Potential', this)">Potential</div>
<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 style="margin-bottom: 0 !important;">
<div class="lead-header sales-toolbar">
<!-- LEFT SIDE -->
<div class="filter-tabs">
<div class="tab active" data-filter="all" onclick="setFilter('all', this)">All</div>
<div class="tab" data-filter="New" onclick="setFilter('New', this)">New</div>
<div class="tab" data-filter="Potential" onclick="setFilter('Potential', this)">Potential</div>
<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>
<!-- RIGHT SIDE -->
<div class="lead-actions">
<input type="text" class="search-input" id="mainSearch"
placeholder="Search leads..." onkeyup="fetchLeads(false)">
<select id="memberFilter" class="toolbar-select searchable" onchange="fetchLeads(false)">
<option value="" selected>All Member</option>
<?php foreach(($sales_manager_with_head ?? []) as $sm): ?>
<option value="<?= $sm['id'] ?>"><?= esc($sm['first_name']) ?></option>
<?php endforeach; ?>
</select>
<select id="financialYearFilter" class="toolbar-select financial-year-select" onchange="fetchLeads(false)">
<?php foreach(($fin_years ?? []) as $year): ?>
<option value="<?= esc($year) ?>" <?= (($current_fin_year ?? '') === $year) ? 'selected' : '' ?>><?= esc($year) ?></option>
<?php endforeach; ?>
</select>
<button class="toolbar-icon-btn export-btn" onclick="exportLeadsCsv()" title="Export" aria-label="Export leads">
<i class="mdi mdi-file-excel"></i>
</button>
<button class="toolbar-icon-btn add-btn" onclick="openModal('addLeadModal')" title="Add Lead" aria-label="Add lead">
<i class="mdi mdi-plus"></i>
</button>
</div>
</div>
<div class="leads-grid" id="leadsGrid"></div>
<div class="row">
<div class="col-lg-12">
<div class="text-center my-3">
<a href="javascript:void(0);" id="btn-load-more" class="text-danger" onclick="fetchLeads(true)">
<span id="load-more-spinner" style="display: none;">
<i class="mdi mdi-spin mdi-loading font-20 align-middle mr-2"></i>
</span>
<span id="load-more-text">Load more</span>
</a>
</div>
</div>
</div>
</div>
<div class="modal" id="addLeadModal">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Add Lead</h4>
<button class="btn-close" onclick="closeModal('addLeadModal')" title="Close">×</button>
</div>
<form id="addLeadForm" enctype="multipart/form-data" novalidate>
<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" style="position:relative;"> -->
<!-- Label with inline short name badge -->
<!-- <label class="form-label">
Company Name<span class="text-danger">*</span>
&nbsp;
<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;">&times;</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> -->
<!-- Save-in-Client: not used here; company is stored on sales_actual_leads.company_name (client_id stripped in JS). -->
<div class="col-xl-12 col-lg-12 col-md-12" style="position:relative;">
<label class="form-label">Company Name<span class="text-danger">*</span></label>
<input type="text" id="add_lead_company_name" class="form-control" name="company_name" style="width: 100%;" placeholder="Enter company name" maxlength="255" required oninput="validateAddLeadField('company_name')">
<small id="add_lead_company_name_error" class="text-danger d-block mt-1 field-error"></small>
</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" id="add_lead_email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\\-_+]/g, ''); validateAddLeadField('email')">
<small id="add_lead_email_error" class="text-danger d-block mt-1 field-error"></small>
</div>
<div class="col-md-12" style="width: 94%;">
<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" id="add_lead_phone" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" maxlength="10" oninput="this.value = this.value.replace(/[^0-9+\\s]/g, ''); validateAddLeadField('phone')">
<small id="add_lead_phone_error" class="text-danger d-block mt-1 field-error"></small>
</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 id="add_lead_status" name="status" class="form-control" style="width: 100%;" required onchange="validateAddLeadField('status')">
<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>
<small id="add_lead_status_error" class="text-danger d-block mt-1 field-error"></small>
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Assign To<span class="text-danger">*</span></label>
<select id="add_lead_assigned_to" name="assigned_to" class="form-control searchable" style="width: 100%;" required onchange="validateAddLeadField('assigned_to')">
<option value="">Select User</option>
<?php foreach($sales_manager_with_head as $sm): ?>
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
<?php endforeach; ?>
</select>
<small id="add_lead_assigned_to_error" class="text-danger d-block mt-1 field-error"></small>
</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('addLeadModal')">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">
<div>
<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>
</div>
<hr class="my-0">
<div class="modal-body">
<div style="background:#f8f8f8; padding:20px; border-radius:12px; margin-bottom:20px; display:grid; grid-template-columns: repeat(4, 1fr); gap:15px;">
<div id="box_email"><small style="color:#999">Email</small><div id="det_email" style="font-weight:500"></div></div>
<div id="box_phone"><small style="color:#999">Phone</small><div id="det_phone" style="font-weight:500"></div></div>
<div id="box_owner"><small style="color:#999">Assigned To</small><div id="det_owner" style="font-weight:500"></div></div>
<div id="box_website"><small style="color:#999">Website</small><div id="det_website" style="font-weight:500"></div></div>
<!-- Full width row -->
<div id="box_address" style="grid-column: span 4;"><small style="color:#999">Address</small><div id="det_address" style="font-weight:500"></div></div>
</div>
<div style="display:flex; border-bottom:1px solid #eee; margin-bottom:20px; gap:30px;">
<h3 id="tab_activity" class="tab-item active" onclick="switchTab('activity')">Activities</h3>
<h3 id="tab_opportunities" class="tab-item" onclick="switchTab('opportunities')">Opportunities</h3>
</div>
<div id="content_activity" class="tab-details">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
<h4 style="margin:0; color:#333; font-size:14px; text-transform:uppercase; letter-spacing:0.5px;">Activity TimeLine</h4>
<button class="btn-primary" style="padding:6px 15px; font-size:12px;" onclick="openActivityModal()">+ Add Activity</button>
</div>
<div id="timelineContainer" class="timeline"></div>
</div>
<div id="content_opportunities" class="tab-details" style="display:none;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
<h4 style="margin:0; color:#333; font-size:14px; text-transform:uppercase; letter-spacing:0.5px;">Opportunity</h4>
<button class="btn-primary" style="padding:6px 15px; font-size:12px; display:none;" onclick="openOpportunityModal()" id="btn_add_opportunity"> + Add Opportunity </button>
</div>
<div id="opportunitiesContainer" class="Opportunities"></div>
</div>
</div>
</div>
</div>
<div class="modal" id="activityModal">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>Add Activity</h2>
<button class="btn-close" onclick="closeModal('activityModal')" title="Close">×</button>
</div>
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
<hr class="my-0">
<div class="modal-body p-4">
<input type="hidden" id="act_lead_id">
<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 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 Docs" onclick="selectType('Share Docs', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="To Do" onclick="selectType('To Do', this)">✓ To Do</button>
</div>
<div class="form-group">
<label class="form-label">Notes <span class="text-danger">*</span></label>
<textarea id="act_notes" class="search-input" style="width:100%; height:100px;" placeholder="Add notes about this activity..." required></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Scheduled Date & Time <span class="text-danger">*</span></label>
<!-- <input type="datetime-local" id="act_date" class="search-input" style="width:100%" required> -->
<div class="input-icon">
<input type="text" class="search-input forschedule" id="act_date" style="width:100%" required placeholder="DD/MM/YYYY hh:mm:ss AM(or)PM" required>
<i class="mdi mdi-calendar-blank-outline additional-icon"></i>
</div>
</div>
<div class="form-group">
<label class="form-label">Assigned To</label>
<select id="act_owner" class="search-input searchable" style="width:100%">
<?php foreach($sales_manager_with_head as $sm): ?>
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Additional Assigner</label>
<select id="act_multiple_owner" class="search-input searchable multi-searchable" style="width:100%;" multiple>
<?php foreach($sales_team as $sm): ?>
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="modal-footer">
<div class="form-group text-right mb-0">
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('activityModal')">Cancel</button>
<button type="submit" class="btn-primary">Create Activity</button>
</div>
</div>
</form>
</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-close" onclick="closeModal('completeModal')" title="Close">×</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 class="form-group">
<label class="form-label">Next Follow-up? <span class="text-danger">*</span></label>
<select id="do_follow" class="search-input" onchange="document.getElementById('f_up').style.display=this.value==='yes'?'block':'none'" style="width:100%;" required>
<option value="no">No</option>
<option value="yes">Yes</option>
</select>
</div>
<div id="f_up" style="display:none; border-top:1px dashed #ccc; padding-top:15px;">
<label class="form-label">Next Activity Type <span class="text-danger">*</span></label>
<div class="activity-types" id="f_typeButtons">
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Call', this)">📞 Call</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Email', this)">✉️ Email</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Demo', this)">🖥️ Demo</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Share Docs', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('To Do', this)">✓ To Do</button>
</div>
<div class="form-group">
<label class="form-label">Follow-up Notes <span class="text-danger">*</span></label>
<textarea id="f_notes" class="search-input" style="width:100%; height:80px;"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Scheduled Date & Time <span class="text-danger">*</span></label>
<div class="input-icon">
<input type="text" class="search-input forschedule" id="f_date" style="width:100%;" placeholder="DD/MM/YYYY hh:mm:ss AM(or)PM">
<i class="mdi mdi-calendar-blank-outline additional-icon"></i>
</div>
</div>
<div class="form-group">
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
<?php foreach($sales_manager_with_head as $sm): ?>
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Additional Assigner <span class="text-danger">*</span></label>
<select id="f_multiple_owner" class="search-input searchable multi-searchable" style="width:100%" multiple>
<?php foreach($sales_team as $sm): ?>
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<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>
<div class="modal" id="opportunityModal">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<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">
<div class="modal-body p-4">
<input type="hidden" id="opp_lead_id">
<div class="radio-option selected" onclick="selectOption(this, 'lead_eb')">
<div class="form-check">
<input class="form-check-input" type="radio" name="lead_form_type" id="lead_eb" value="1" checked>
<label class="form-check-label" for="lead_eb">EB</label>
</div>
</div>
<div class="radio-option" onclick="selectOption(this, 'lead_non_eb')">
<div class="form-check">
<input class="form-check-input" type="radio" name="lead_form_type" id="lead_non_eb" value="2">
<label class="form-check-label" for="lead_non_eb">Non-EB</label>
</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('opportunityModal')">Cancel</button>
<button type="button" class="btn-primary" onclick="submitToRedirectwithactualLeadIDUrl(this)">Create Opportunity</button>
</div>
</div>
</div>
</div>
<div class="modal" id="editLeadModal">
<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>
</div>
<form id="editLeadForm" enctype="multipart/form-data" novalidate>
<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">
<!-- Save-in-Client: not used here; company is stored on sales_actual_leads.company_name (client_id stripped in JS). -->
<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" id="edit_lead_company_name" class="form-control" name="company_name" style="width: 100%;" placeholder="Enter company name" maxlength="255" required oninput="validateEditLeadField('company_name')">
<small id="edit_lead_company_name_error" class="text-danger d-block mt-1 field-error"></small>
</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>
&nbsp;
<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;">&times;</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%;">
<label class="form-label">Email</label>
<input type="email" id="edit_lead_email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\-_+]/g, ''); validateEditLeadField('email')">
<small id="edit_lead_email_error" class="text-danger d-block mt-1 field-error"></small>
</div>
<div class="col-md-12" style="width: 94%;">
<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" id="edit_lead_phone" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" maxlength="10" oninput="this.value = this.value.replace(/[^0-9+\s]/g, ''); validateEditLeadField('phone')">
<small id="edit_lead_phone_error" class="text-danger d-block mt-1 field-error"></small>
</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" id="edit_lead_gst_number" 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(); validateEditLeadField('gst_number')" maxlength="15"
pattern="^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$">
<small id="edit_lead_gst_number_error" class="text-danger d-block mt-1 field-error"></small>
</div>
</div>
<style>
.g-0-5 { --bs-gutter-x: 0.3rem; }
.g-0-5 > * { padding-right: calc(var(--bs-gutter-x) * .5); padding-left: calc(var(--bs-gutter-x) * .5); }
.primary-container {
display: flex;
flex-direction: column-reverse;
align-items: center;
justify-content: center;
/* Ensures the checkbox container height matches the standard input height */
height: 31px;
}
.primary-label {
font-size: 12px !important;
font-weight: bold;
/* Forces the text to take up exactly its own height */
line-height: 1;
margin-top: 2px;
color: #888;
display: block;
}
/* This is the most important part */
.primary-container .form-check-input {
margin: 0 !important;
padding: 0 !important;
cursor: pointer;
/* Reset Bootstrap's default position: absolute if present */
position: static;
}
</style>
<div class="col-12 ml-1">
<label class="form-label fw-bold small ml-1">Contact Persons</label>
<input type="hidden" id="editing_contact_id" value="">
<div class="row g-0-5 align-items-end ml-1">
<div class="col-md-3 col-12 mb-2 mb-md-0">
<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, ''); validateContactField('name')">
<small id="contact_name_error" class="text-danger d-block mt-1 field-error"></small>
</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); validateContactField('mobile')">
<small id="contact_mobile_error" class="text-danger d-block mt-1 field-error"></small>
</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">
<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-danger text-white" style="width:42%" title="Clear">
</button>
</div>
</div>
</div>
<div id="contactPersonsList" class="p-1">
<div id="noContactPersonsData" class="py-1 text-center">
<small class="text-muted fst-italic">No contact persons added yet</small>
</div>
</div>
</div>
<div 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 id="edit_lead_status" name="status" class="form-control" style="width: 100%;" required onchange="validateEditLeadField('status')">
<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>
<small id="edit_lead_status_error" class="text-danger d-block mt-1 field-error"></small>
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Assign To <span class="text-danger">*</span> </label>
<select id="edit_lead_assigned_to" name="assigned_to" class="form-control searchable" style="width: 100%;" required onchange="validateEditLeadField('assigned_to')">
<option value="">Select User</option>
<?php foreach($sales_manager_with_head as $sm): ?>
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
<?php endforeach; ?>
</select>
<small id="edit_lead_assigned_to_error" class="text-danger d-block mt-1 field-error"></small>
</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.addEventListener('DOMContentLoaded', function () {
resetFlatpicker();
// Single selects
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
let parentModal = el.closest('.modal');
const select2Options = {
allowClear: false,
minimumResultsForSearch: 0,
dropdownParent: parentModal ? $(parentModal) : $(document.body)
};
if (el.id !== 'memberFilter') {
select2Options.placeholder = "Select..";
}
$(el).select2(select2Options);
});
// Multi selects
document.querySelectorAll('.multi-searchable').forEach(function(el) {
let parentModal = el.closest('.modal');
$(el).select2({
placeholder: "Search and select members...",
allowClear: false,
closeOnSelect: false,
minimumResultsForSearch: 0,
dropdownParent: parentModal ? $(parentModal) : $(document.body),
width: '100%'
});
});
});
function resetFlatpicker(){
document.querySelectorAll(".forschedule").forEach(el => {
if (el._flatpickr) {
el._flatpickr.destroy();
}
});
flatpickr(".forschedule", {
enableTime: true,
enableSeconds: true,
time_24hr: false,
dateFormat: "d/m/Y h:i:S K",
allowInput: false,
});
}
const salesManagerHeadIds = <?= json_encode($sales_manager_with_head_ids ?? []) ?>;
const API = '<?= base_url('sales') ?>';
const SALES_CHECK_DUPLICATE_URL = '<?= base_url('sales/checkDuplicate') ?>';
/** Normalized company name when edit modal opened — same name retyped after clear must not count as duplicate */
let trackerEditLeadOriginalCompanyNorm = '';
const defaultFinancialYear = '<?= esc($current_fin_year ?? '') ?>';
let filter = 'all';
let lead_id = null;
let global_lead_assigned_to = null;
let selectedType = 'Call';
let selectedFollowUpActivityType = '';
let currentPage = 1;
let limit = 9;
let currentOffset = 0;
function getCurrentFY() {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
const startYear = month >= 3 ? year : year - 1;
return startYear + '-' + String(startYear + 1);
}
function initSalesToolbarFilters() {
const fySelect = document.getElementById('financialYearFilter');
const params = new URLSearchParams(window.location.search);
if (fySelect) {
let fy = params.get('fy') || defaultFinancialYear || getCurrentFY();
const matched = Array.from(fySelect.options).some(opt => opt.value === fy);
if (!matched && fySelect.options.length > 0) {
fy = fySelect.options[0].value;
}
fySelect.value = fy;
}
const statusParam = (params.get('status') || 'all').toLowerCase();
const statusTab = Array.from(document.querySelectorAll('.filter-tabs .tab'))
.find(tab => (tab.dataset.filter || '').toLowerCase() === statusParam);
if (statusTab) {
document.querySelectorAll('.filter-tabs .tab').forEach(tab => tab.classList.remove('active'));
statusTab.classList.add('active');
filter = statusTab.dataset.filter || 'all';
}
const memberSelect = document.getElementById('memberFilter');
const memberId = params.get('member');
if (memberSelect && memberId && Array.from(memberSelect.options).some(opt => opt.value === memberId)) {
memberSelect.value = memberId;
if (window.jQuery) {
$(memberSelect).trigger('change.select2');
}
}
}
function getSelectedMemberIds(defaultIds) {
const memberFilter = document.getElementById('memberFilter');
if (memberFilter && memberFilter.value) {
return [memberFilter.value];
}
return Array.isArray(defaultIds) ? defaultIds : [];
}
function getSelectedFinancialYear() {
return document.getElementById('financialYearFilter')?.value || defaultFinancialYear || getCurrentFY();
}
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);
if (modal) {
modal.classList.remove('active'); // Hide the modal
const form = modal.querySelector('form'); // Find the form inside this specific modal
if (form) {
form.reset(); // Resets standard inputs (text, email, select)
form.querySelectorAll('.searchable').forEach(function(el) {
// Reset native select
Array.from(el.options).forEach(o => o.selected = false);
// Notify Select2 to refresh its UI
$(el).trigger('change');
});
$(form).find('.is-invalid').removeClass('is-invalid'); // Remove any "is-invalid" red borders from previous validation errors
}
// Specific cleanup for your "Activity" logic
if (id === 'activityModal') {
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');
if (fUpSection) fUpSection.style.display = 'none';
selectedFollowUpActivityType = ''; // Reset your global activity type variable
$('.f_activity_type').removeClass('active');
}
// Specific action for leadDetailModal
if (id === 'leadDetailModal') {
switchTab('activity'); // Reset the tab back to 'activity'
document.getElementById('btn_add_opportunity').style.display = 'none';
}
if (id === 'addLeadModal') {
clearAddLeadFieldErrors();
}
if (id === 'editLeadModal') {
clearEditLeadFieldErrors();
setContactFieldError('name', '');
setContactFieldError('mobile', '');
}
// 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') {
// form.querySelectorAll('[name="status"] option').forEach(opt => opt.hidden = false);
// form.querySelector('[name="status"]').value = 'New';
// _resetCompanyUI('edit');
// }
}
}
function selectType(val, el) {
document.querySelectorAll('.activity-type-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
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;
setContactFieldError('name', '');
setContactFieldError('mobile', '');
document.getElementById('btnSaveContact').innerHTML = "✔";
document.getElementById('btnSaveContact').title = "Save";
document.getElementById('btnSaveContact').style.background = "#02a8b5";
}
function setContactFieldError(field, message) {
const map = {
name: { input: 'contact_name', error: 'contact_name_error' },
mobile: { input: 'contact_mobile', error: 'contact_mobile_error' }
};
const cfg = map[field];
if (!cfg) return;
const input = document.getElementById(cfg.input);
const errorEl = document.getElementById(cfg.error);
if (!input || !errorEl) return;
errorEl.textContent = message || '';
if (message) input.classList.add('is-invalid');
else input.classList.remove('is-invalid');
}
function validateContactField(field) {
const name = (document.getElementById('contact_name')?.value || '').trim();
const mobile = (document.getElementById('contact_mobile')?.value || '').trim();
if (field === 'name') {
const msg = name ? '' : 'Contact person name is required';
setContactFieldError('name', msg);
return !msg;
}
if (field === 'mobile') {
const msg = mobile ? '' : 'Mobile number is required';
setContactFieldError('mobile', msg);
return !msg;
}
return true;
}
function validateContactForm() {
const nameOk = validateContactField('name');
const mobileOk = validateContactField('mobile');
if (!nameOk) {
document.getElementById('contact_name')?.focus();
return false;
}
if (!mobileOk) {
document.getElementById('contact_mobile')?.focus();
return false;
}
return true;
}
// 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'));
el.classList.add('active');
selectedFollowUpActivityType = val;
}
// Tab Change
function setFilter(val, el) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
filter = val;
const searchInput = document.getElementById('mainSearch');
if (searchInput) {
searchInput.value = '';
}
fetchLeads(false);
}
// 1. Leads Grid Logic
async function fetchLeads_1stold() {
const q = document.getElementById('mainSearch').value;
const res = await fetch(`${API}/leads?status=${filter==='all'?'':filter}&search=${q}`);
const json = await res.json();
document.getElementById('leadsGrid').innerHTML = json.data.map(l => `
<div class="lead-card" onclick="viewDetail(${l.lead_id})">
<div style="font-weight:600; font-size:17px;">${l.company_name}</div>
<span class="lead-status status-${l.status.toLowerCase().replace(' ', '-')}">${l.status}</span>
<div style="margin-top:12px; color:#666; font-size:13px;">
<div>✉️ ${l.email}</div>
<div>📞 ${l.phone}</div>
</div>
<div style="margin-top:15px; border-top:1px solid #f0f0f0; padding-top:10px; font-size:12px;">
Assigned to: <b>${l.assigned_to_name || 'Unassigned'}</b>
</div>
</div>
`).join('');
}
async function fetchLeads(isLoadMore = false) {
const q = document.getElementById('mainSearch').value;
const grid = document.getElementById('leadsGrid');
const btnLoadMore = document.getElementById('btn-load-more');
const spinner = document.getElementById('load-more-spinner');
const text = document.getElementById('load-more-text');
const selectedMemberIds = getSelectedMemberIds(salesManagerHeadIds);
// 1. Define the empty state HTML early so we can use it immediately if needed
const emptyStateHTML = `<div style="display: contents;">
<div class="empty-state" style="grid-column: 1 / -1; text-align:center;">
<div class="empty-icon">📋</div>
<p>No leads found</p>
</div>
</div>`;
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
if (selectedMemberIds.length === 0) {
console.log("No Sales Manager IDs found. Skipping API call.");
grid.innerHTML = emptyStateHTML;
btnLoadMore.style.display = 'none'; // Hide the load more button
// Reset spinner just in case
if (isLoadMore) {
spinner.style.display = 'none';
text.innerText = 'Load more';
}
return; // This completely stops the function here. No API call is made.
}
// 3. If we are doing a fresh search or changing tabs, reset offset to 0
if (!isLoadMore) {
currentOffset = 0;
}
// 4. Show spinner on button
if (isLoadMore) {
spinner.style.display = 'inline-block';
text.innerText = 'Loading...';
}
// 5. Build URL with dynamic offset
const params = new URLSearchParams({
status: filter === 'all' ? '' : filter,
search: q,
limit,
offset: currentOffset,
assigned_to: selectedMemberIds.join(','),
financial_year: getSelectedFinancialYear()
});
let url = `${API}/leads?${params.toString()}`;
console.log("Fetching API:", url);
try {
const res = await fetch(url);
const json = await res.json();
const leadsData = json.data || [];
const totalrecords = json.total || 0;
console.log("API Response:", leadsData);
const tabLeadsCount = json.counts || {};
// ✅ Update tab counts
if (tabLeadsCount) {
document.querySelector('[data-filter="all"]').innerText = `All (${tabLeadsCount.all ?? 0})`;
document.querySelector('[data-filter="New"]').innerText = `New (${tabLeadsCount.New ?? 0})`;
document.querySelector('[data-filter="Potential"]').innerText = `Potential (${tabLeadsCount.Potential ?? 0})`;
document.querySelector('[data-filter="Prospects"]').innerText = `Prospects (${tabLeadsCount.Prospects ?? 0})`;
document.querySelector('[data-filter="Not a Prospects"]').innerText = `Not a Prospects (${tabLeadsCount['Not a Prospects'] ?? 0})`;
}
const html = leadsData
.sort((a, b) => Number(b.lead_id) - Number(a.lead_id))
.map(l => {
let formattedCreatedDate = l.created_at
? new Date(l.created_at).toLocaleString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
hour: '2-digit', minute: '2-digit', hour12: true
})
: 'N/A';
return `
<div class="lead-card mt-1">
<div class="media align-items-center">
<div class="avatar-md mr-1">
<div class="avatar-title bg-soft-primary rounded-circle text-primary font-20 font-weight-semibold"
style="background-color: #f1f3fa; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center;">
${(l.company_name || '?').charAt(0).toUpperCase()}
</div>
</div>
<div class="media-body">
<h5 class="my-0"><a href="javascript:void(0);" class="text-dark">${l.company_name}</a></h5>
<p class="text-muted mb-0 small">
<span class="lead-status status-${l.status.toLowerCase().replace(/ /g, '-')}">${l.status}</span>
</p>
</div>
<div class="dropdown">
<a class="text-body dropdown-toggle" href="#" data-toggle="dropdown">
<i class="mdi mdi-dots-vertical font-20 btn-close" title="Options"></i>
</a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="javascript:void(0);" onclick="viewDetail(${l.lead_id})"> 👁 View</a>
<a class="dropdown-item" href="javascript:void(0);" onclick="openEditLeadModal(${l.lead_id})"> <i class="ri-pencil-fill"></i> Edit</a>
</div>
</div>
</div>
<div style="margin-top:12px; color:#666; font-size:13px;">
<div>✉️ ${l.email || 'No Email'}</div>
<div>📞 ${l.phone || 'No Phone'}</div>
</div>
<div style="margin-top:15px; border-top:1px solid #f0f0f0; padding-top:10px; font-size:12px; display:flex; justify-content:space-between; align-items:center;">
<div><span class="text-muted">Assigned to : </span><b>${l.assigned_to_name || 'Unassigned'}</b></div>
<div><b>${formattedCreatedDate || 'N/A'}</b></div>
</div>
</div>
`;
}).join('');
// 6. Append if loading more, Replace if fresh search
if (isLoadMore) {
grid.insertAdjacentHTML('beforeend', html);
} else {
grid.innerHTML = html || emptyStateHTML;
}
// 7. Update offset for the NEXT time you click the button
currentOffset += leadsData.length;
// 8. Stop spinner
spinner.style.display = 'none';
text.innerText = 'Load more';
// 9. Hide button if the API returns LESS than our limit
if (leadsData.length < limit || leadsData.length === 0) {
btnLoadMore.style.display = 'none';
} else {
btnLoadMore.style.display = 'inline-block';
}
} catch (e) {
console.error("Error loading leads:", e);
spinner.style.display = 'none';
text.innerText = 'Load more';
}
}
// 2. Detail Logic
async function viewDetail(id) {
lead_id = id;
const res = await fetch(`${API}/leads/${id}`);
const json = await res.json();
const l = json.data;
document.getElementById('det_company').innerText = l.company_name;
document.getElementById('det_email').innerText = l.email || 'N/A';
document.getElementById('det_phone').innerText = l.phone || 'N/A';
global_lead_assigned_to = l.assigned_to;
if (l.address) { document.getElementById('det_address').innerText = l.address
? l.address.replace(/[\r\n]+/g, ', ')
: '';
document.getElementById('box_address').style.display = 'block';
} else { document.getElementById('box_address').style.display = 'none'; }
if (l.website) { document.getElementById('det_website').innerText = l.website;
document.getElementById('box_website').style.display = 'block';
} else { document.getElementById('box_website').style.display = 'none'; }
if (l.assigned_to_name) { document.getElementById('det_owner').innerText = l.assigned_to_name;
document.getElementById('box_owner').style.display = 'block';
} 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(' ', '-')}`;
const oppBtn = document.getElementById('btn_add_opportunity');
if (l.status === 'Prospects') {
oppBtn.style.display = 'inline-block';
} else {
oppBtn.style.display = 'none';
}
renderTimeline(l.activities);
renderCard(l.opportunities || []);
openModal('leadDetailModal');
}
function renderCard(opps) {
const opp_cont = document.getElementById('opportunitiesContainer');
opp_cont.innerHTML = opps.length ? opps.map(o => {
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
let status = o.status?.toLowerCase();
let statusClass = {
won: 'status-text-won',
lost: 'status-text-lost'
}[status] || 'status-text-unknown';
let opp_status_value = o.status?.replace(/[-_']/g, ' ').toUpperCase();
let formattedDate = new Date(o.created_at).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
// ${o.client_short_name}
return `
<div class="opportunities-list" id="leadOpportunitiesList">
<div class="opportunity-card" style="bottom:2px;">
<div class="opportunity-header">
<div class="opportunity-title">${lead_type}</div>
</div>
<div class="opportunity-details">
<div class="opportunity-detail-item">
<strong>Client Name :</strong> ${o.client_name}
</div>
<div class="opportunity-detail-item">
<strong>Client Code:</strong> ${o.client_code}
</div>
<div class="opportunity-detail-item">
<strong>Created At:</strong> ${formattedDate}
</div>
<div class="opportunity-detail-item">
<strong>Status:</strong> <span class="${statusClass}">${opp_status_value}</span>
</div>
</div>
<div class="opportunity-footer">
${ `${o.status === 'lost' ? `
<div class="lost-reason">
<strong>Reason for Loss:</strong> ${o.lost_reason || 'N/A'}
</div>
<div class="footer-divider"></div>
` : ``}
<div class="notes">
<strong>Remarks:</strong> ${o.notes || 'N/A'}
</div>
`
}
</div>
</div>
</div>`;
}).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');
const activityIcons = { "Call": "📞", "Email": "✉️", "Meeting": "📅", "Visit": "🚗", "Demo": "🖥️", "Share Docs": "📄", "To Do": "✓"};
if (acts.length === 0) {
cont.classList.add('no-line');
} else {
cont.classList.remove('no-line');
}
cont.innerHTML = acts.length ? acts.map(a => {
const icon = activityIcons[a.activity_type] || "";
const formattedType = a.activity_type?.toUpperCase() || "";
const formattedDate = new Date(a.scheduled_date).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
// Then use the variable in your HTML:
// <span style="font-size:11px; color:#999">${formattedDate}</span>
return `
<div class="timeline-item">
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
<div class="timeline-content">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:2px;">
<b style="white-space: nowrap; margin-right: 15px;">${icon} ${formattedType}</b>
<span style="font-size:11px; color:#999; text-align:right;"> 👤 ${a.assigned_to_name} | 🗓️ ${formattedDate}</span>
</div>
${a.additional_assigned_names ? `
<div style="display:flex; justify-content:flex-end; margin-bottom:5px;">
<span style="font-size:11px; color:#999; text-align: right;
max-width: 75%; /* Forces long lists to wrap cleanly on the right side */
display: inline-block;">
👥 ${a.additional_assigned_names}
</span>
</div>
` : '<div style="margin-bottom:8px;"></div>'}
<div style="font-size:13px; color:#444; margin-bottom: 8px;">
${a.notes}
</div>
${a.status === 'pending' ?
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:4px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
`<div style="font-size:12px; color:#388e3c; margin-top:4px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
}
</div>
</div>`;
}).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')).trigger('change'); // tells Select2 to update display
// document.getElementById('act_owner').dispatchEvent(new Event('change'));
document.getElementById('act_lead_id').value = lead_id;
openModal('activityModal');
}
function openComp(id, assigned_to, lead_id) {
document.getElementById('completeForm').reset();
document.getElementById('f_notes').removeAttribute('required');
document.getElementById('f_date').removeAttribute('required');
document.getElementById('f_assigned_to').removeAttribute('required');
document.getElementById('comp_id').value = id;
document.getElementById('lead_id').value = lead_id;
document.getElementById('f_assigned_to').value = assigned_to;
$(document.getElementById('f_assigned_to')).trigger('change'); // tells Select2 to update display
// document.getElementById('f_assigned_to').dispatchEvent(new Event('change'));
document.getElementById('f_typeButtons').querySelectorAll('.f_activity_type')
.forEach(btn => btn.classList.remove('active'));
selectedFollowUpActivityType = '';
openModal('completeModal');
}
function openOpportunityModal() {
document.getElementById('opp_lead_id').value = lead_id;
openModal('opportunityModal');
}
/** Case-insensitive duplicate check against sales_actual_leads.company_name (optional exclude lead_id for edit). */
async function checkSalesActualLeadCompanyNameDuplicate(rawName, excludeLeadId) {
const value = (rawName || '').trim();
if (!value) {
return false;
}
let url = `${SALES_CHECK_DUPLICATE_URL}?table=sales_actual_leads&field=company_name&value=${encodeURIComponent(value)}`;
if (excludeLeadId) {
url += `&exclude_id=${encodeURIComponent(String(excludeLeadId))}`;
}
const res = await fetch(url);
if (!res.ok) {
return false;
}
const json = await res.json();
return !!json.exists;
}
function switchTab(tabName) {
//Default Activity Tab how to resset here
// 1. Hide all tab content
const contents = document.querySelectorAll('.tab-details');
contents.forEach(content => {
content.style.display = 'none';
});
// 2. Remove 'active' class from all tab headers
const tabs = document.querySelectorAll('.tab-item');
tabs.forEach(tab => {
tab.classList.remove('active');
});
// 3. Show the specific content for the clicked tab
document.getElementById('content_' + tabName).style.display = 'block';
// 4. Add 'active' class to the clicked tab header
document.getElementById('tab_' + tabName).classList.add('active');
}
// Add Lead Form Submissions
function setAddLeadFieldError(fieldName, message) {
const map = {
company_name: { input: 'add_lead_company_name', error: 'add_lead_company_name_error' },
email: { input: 'add_lead_email', error: 'add_lead_email_error' },
phone: { input: 'add_lead_phone', error: 'add_lead_phone_error' },
status: { input: 'add_lead_status', error: 'add_lead_status_error' },
assigned_to: { input: 'add_lead_assigned_to', error: 'add_lead_assigned_to_error' }
};
const cfg = map[fieldName];
if (!cfg) return;
const input = document.getElementById(cfg.input);
const errEl = document.getElementById(cfg.error);
if (!input || !errEl) return;
errEl.textContent = message || '';
if (message) {
input.classList.add('is-invalid');
} else {
input.classList.remove('is-invalid');
}
}
function clearAddLeadFieldErrors() {
['company_name', 'email', 'phone', 'status', 'assigned_to'].forEach((field) => setAddLeadFieldError(field, ''));
}
function getAddLeadValidationError(fieldName) {
const form = document.getElementById('addLeadForm');
if (!form) return '';
const companyName = (form.querySelector('[name="company_name"]')?.value || '').trim();
const email = (form.querySelector('[name="email"]')?.value || '').trim();
const phone = (form.querySelector('[name="phone"]')?.value || '').trim();
const companyRegex = /^[a-zA-Z0-9\s_-]+$/;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const phoneRegex = /^[0-9+\s]+$/;
if (fieldName === 'company_name') {
if (!companyName) return 'Company Name is Missing';
if (!companyRegex.test(companyName)) return 'Company Name can only contain letters, numbers.';
if (companyName.length < 2) return 'Company Name must be at least 2 characters long';
if (companyName.length > 255) return 'Company Name must be at most 255 characters long';
return '';
}
if (fieldName === 'email') {
if (!email && !phone) return 'Either Email or Mobile Number is Needed.';
if (email && email.length > 255) return 'Email must be at most 255 characters long';
if (email && !emailRegex.test(email)) return 'Please provide a valid email address';
return '';
}
if (fieldName === 'phone') {
if (!email && !phone) return 'Either Email or Mobile Number is Needed.';
if (phone && !phoneRegex.test(phone)) return 'Mobile number can contain only digits, + and spaces.';
if (phone && phone.length < 10) return 'Mobile Number must be at least 10 characters long';
if (phone && phone.length > 20) return 'Mobile Number must be at most 20 characters long';
return '';
}
if (fieldName === 'status') {
const status = (form.querySelector('[name="status"]')?.value || '').trim();
if (!status) return 'Please select a valid Status';
return '';
}
if (fieldName === 'assigned_to') {
const assignedTo = (form.querySelector('[name="assigned_to"]')?.value || '').trim();
if (!assignedTo) return 'Please select a valid User';
return '';
}
return '';
}
function validateAddLeadField(fieldName) {
// email and phone are inter-dependent (required_without each other)
if (fieldName === 'email' || fieldName === 'phone') {
const emailMessage = getAddLeadValidationError('email');
const phoneMessage = getAddLeadValidationError('phone');
setAddLeadFieldError('email', emailMessage);
setAddLeadFieldError('phone', phoneMessage);
return !emailMessage && !phoneMessage;
}
const message = getAddLeadValidationError(fieldName);
setAddLeadFieldError(fieldName, message);
return !message;
}
function validateAddLeadForm() {
clearAddLeadFieldErrors();
const fields = ['company_name', 'email', 'phone', 'status', 'assigned_to'];
let firstInvalid = null;
for (const field of fields) {
const message = getAddLeadValidationError(field);
if (message) {
setAddLeadFieldError(field, message);
if (!firstInvalid) firstInvalid = field;
}
}
if (firstInvalid) {
const focusMap = {
company_name: 'add_lead_company_name',
email: 'add_lead_email',
phone: 'add_lead_phone',
status: 'add_lead_status',
assigned_to: 'add_lead_assigned_to'
};
const el = document.getElementById(focusMap[firstInvalid]);
if (el) el.focus();
return false;
}
return true;
}
function showAddLeadSubmitError(message, fieldName) {
setAddLeadFieldError(fieldName, message);
const focusMap = {
company_name: 'add_lead_company_name',
email: 'add_lead_email',
phone: 'add_lead_phone',
status: 'add_lead_status',
assigned_to: 'add_lead_assigned_to'
};
const fallback = document.querySelector('#addLeadForm [name="' + fieldName + '"]');
const target = document.getElementById(focusMap[fieldName]) || fallback;
if (target) {
setTimeout(() => target.focus(), 80);
}
}
// Safety: bind listeners in JS as well (besides inline oninput)
document.addEventListener('DOMContentLoaded', function () {
const companyEl = document.getElementById('add_lead_company_name');
const emailEl = document.getElementById('add_lead_email');
const phoneEl = document.getElementById('add_lead_phone');
const statusEl = document.getElementById('add_lead_status');
const assignedEl = document.getElementById('add_lead_assigned_to');
if (companyEl) companyEl.addEventListener('input', function () { validateAddLeadField('company_name'); });
if (emailEl) emailEl.addEventListener('input', function () { validateAddLeadField('email'); });
if (phoneEl) phoneEl.addEventListener('input', function () { validateAddLeadField('phone'); });
if (statusEl) statusEl.addEventListener('change', function () { validateAddLeadField('status'); });
if (assignedEl) assignedEl.addEventListener('change', function () { validateAddLeadField('assigned_to'); });
const editCompanyEl = document.getElementById('edit_lead_company_name');
const editEmailEl = document.getElementById('edit_lead_email');
const editPhoneEl = document.getElementById('edit_lead_phone');
const editGstEl = document.getElementById('edit_lead_gst_number');
const editStatusEl = document.getElementById('edit_lead_status');
const editAssignedEl = document.getElementById('edit_lead_assigned_to');
if (editCompanyEl) editCompanyEl.addEventListener('input', function () { validateEditLeadField('company_name'); });
if (editEmailEl) editEmailEl.addEventListener('input', function () { validateEditLeadField('email'); });
if (editPhoneEl) editPhoneEl.addEventListener('input', function () { validateEditLeadField('phone'); });
if (editGstEl) editGstEl.addEventListener('input', function () { validateEditLeadField('gst_number'); });
if (editStatusEl) editStatusEl.addEventListener('change', function () { validateEditLeadField('status'); });
if (editAssignedEl) editAssignedEl.addEventListener('change', function () { validateEditLeadField('assigned_to'); });
});
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();
// 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');
// }
// --- Field-level validation ---
if (!validateAddLeadForm()) {
toastr.warning('Please correct highlighted fields.', 'Validation Error');
return;
}
try {
const dupCompany = await checkSalesActualLeadCompanyNameDuplicate(companyName, null);
if (dupCompany) {
toastr.warning('A lead with this company name already exists.', 'Duplicate company');
const inp = document.getElementById('add_lead_company_name');
if (inp) {
inp.value = '';
inp.focus();
}
return;
}
} catch (dupErr) {
toastr.error('Could not verify company name. Please try again.');
return;
}
// Save-in-Client (sync to `clients`): disabled — company lives on sales_actual_leads.company_name only
delete data.client_id;
delete data.short_name;
// --- 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('addLeadModal');
fetchLeads();
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]) => {
if (field === 'company_name' || field === 'email' || field === 'phone' || field === 'status' || field === 'assigned_to') {
setAddLeadFieldError(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;
}
};
function setEditLeadFieldError(fieldName, message) {
const map = {
company_name: { input: 'edit_lead_company_name', error: 'edit_lead_company_name_error' },
email: { input: 'edit_lead_email', error: 'edit_lead_email_error' },
phone: { input: 'edit_lead_phone', error: 'edit_lead_phone_error' },
gst_number: { input: 'edit_lead_gst_number', error: 'edit_lead_gst_number_error' },
status: { input: 'edit_lead_status', error: 'edit_lead_status_error' },
assigned_to: { input: 'edit_lead_assigned_to', error: 'edit_lead_assigned_to_error' }
};
const cfg = map[fieldName];
if (!cfg) return;
const input = document.getElementById(cfg.input);
const errEl = document.getElementById(cfg.error);
if (!input || !errEl) return;
errEl.textContent = message || '';
if (message) input.classList.add('is-invalid');
else input.classList.remove('is-invalid');
}
function clearEditLeadFieldErrors() {
['company_name', 'email', 'phone', 'gst_number', 'status', 'assigned_to'].forEach((field) => setEditLeadFieldError(field, ''));
}
function getEditLeadValidationError(fieldName) {
const form = document.getElementById('editLeadForm');
if (!form) return '';
const companyName = (form.querySelector('[name="company_name"]')?.value || '').trim();
const email = (form.querySelector('[name="email"]')?.value || '').trim();
const phone = (form.querySelector('[name="phone"]')?.value || '').trim();
const gst = (form.querySelector('[name="gst_number"]')?.value || '').trim();
const status = (form.querySelector('[name="status"]')?.value || '').trim();
const assignedTo = (form.querySelector('[name="assigned_to"]')?.value || '').trim();
const companyRegex = /^[a-zA-Z0-9\s_-]+$/;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const phoneRegex = /^[0-9+\s]+$/;
const gstRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/;
if (fieldName === 'company_name') {
if (!companyName) return 'Company Name is Missing';
if (!companyRegex.test(companyName)) return 'Company Name can only contain letters, numbers.';
if (companyName.length < 2) return 'Company Name must be at least 2 characters long';
if (companyName.length > 255) return 'Company Name must be at most 255 characters long';
return '';
}
if (fieldName === 'email') {
if (!email && !phone) return 'Either Email or Mobile Number is Needed.';
if (email && !emailRegex.test(email)) return 'Please provide a valid email address';
if (email && email.length > 255) return 'Email must be at most 255 characters long';
return '';
}
if (fieldName === 'phone') {
if (!email && !phone) return 'Either Email or Mobile Number is Needed.';
if (phone && !phoneRegex.test(phone)) return 'Mobile number can contain only digits, + and spaces.';
if (phone && phone.length < 10) return 'Mobile Number must be at least 10 characters long';
if (phone && phone.length > 20) return 'Mobile Number must be at most 20 characters long';
return '';
}
if (fieldName === 'gst_number') {
if (!gst) return '';
if (gst.length !== 15) return 'GST Number must be exactly 15 characters long.';
if (!gstRegex.test(gst)) return 'Please enter a valid GST Number (e.g., 22AAAAA0000A1Z5).';
return '';
}
if (fieldName === 'status') {
if (!status) return 'Please select a valid Status';
return '';
}
if (fieldName === 'assigned_to') {
if (!assignedTo) return 'Please select a valid User';
return '';
}
return '';
}
function validateEditLeadField(fieldName) {
if (fieldName === 'email' || fieldName === 'phone') {
const emailMessage = getEditLeadValidationError('email');
const phoneMessage = getEditLeadValidationError('phone');
setEditLeadFieldError('email', emailMessage);
setEditLeadFieldError('phone', phoneMessage);
return !emailMessage && !phoneMessage;
}
const message = getEditLeadValidationError(fieldName);
setEditLeadFieldError(fieldName, message);
return !message;
}
function validateEditLeadForm() {
clearEditLeadFieldErrors();
const fields = ['company_name', 'email', 'phone', 'gst_number', 'status', 'assigned_to'];
let firstInvalid = null;
fields.forEach((field) => {
const msg = getEditLeadValidationError(field);
if (msg) {
setEditLeadFieldError(field, msg);
if (!firstInvalid) firstInvalid = field;
}
});
if (firstInvalid) {
const focusMap = {
company_name: 'edit_lead_company_name',
email: 'edit_lead_email',
phone: 'edit_lead_phone',
gst_number: 'edit_lead_gst_number',
status: 'edit_lead_status',
assigned_to: 'edit_lead_assigned_to'
};
const el = document.getElementById(focusMap[firstInvalid]);
if (el) el.focus();
return false;
}
return true;
}
// 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;
// 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);
let leadId = data.lead_id?.trim();
let companyName = data.company_name?.trim();
// --- 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 (!validateEditLeadForm()) {
toastr.warning('Please correct highlighted fields.', 'Validation Error');
return;
}
const normEntered = companyName.trim().toLowerCase();
if (normEntered !== trackerEditLeadOriginalCompanyNorm) {
try {
const dupCompany = await checkSalesActualLeadCompanyNameDuplicate(companyName, leadId);
if (dupCompany) {
toastr.warning('A lead with this company name already exists.', 'Duplicate company');
return;
}
} catch (dupErr) {
toastr.error('Could not verify company name. Please try again.');
return;
}
}
// Save-in-Client (sync to `clients`): disabled — company lives on sales_actual_leads.company_name only
delete data.client_id;
delete data.short_name;
// --- 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');
e.target.reset();
closeModal('editLeadModal');
fetchLeads();
// CLEANUP: Reset the contact UI for the next time it opens
$('#contactPersonsList').find('.contact-card').remove();
$('#noContactPersonsData').show();
document.getElementById('editing_contact_id').value = '';
document.getElementById('btnSaveContact').innerHTML = "✔";
document.getElementById('btnSaveContact').title = "Save";
document.getElementById('btnSaveContact').style.background = "#02a8b5";
} 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]) => {
if (field === 'company_name' || field === 'email' || field === 'phone' || field === 'gst_number' || field === 'status' || field === 'assigned_to') {
setEditLeadFieldError(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();
// 1. Gather values first
let feFormat = document.getElementById('act_date').value;
let actNotes = document.getElementById('act_notes').value.trim();
let actOwner = document.getElementById('act_owner').value.trim();
// 2. Perform Validation
if (!actNotes) {
toastr.warning('Notes is required');
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 an activity type.');
return;
}
if (!feFormat.trim()) {
toastr.warning('Scheduled date & time is required');
return;
}
if (!actOwner) {
toastr.warning('Please Select the user to be Assigned');
return;
}
// 3. Validation passed! Now disable the button and change text
const submitBtn = e.target.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerText;
submitBtn.disabled = true;
submitBtn.innerText = 'Creating...';
// 4. Format date and prep payload
let dbFormat = convertDBFormatted(feFormat);
try {
const payload = {
lead_id: document.getElementById('act_lead_id').value,
activity_type: selectedType,
notes: actNotes, // Reused the trimmed variable from above
scheduled_date: dbFormat,
assigned_to: actOwner, // Reused the trimmed variable from above
status: 'pending',
additional_assigned_ids : Array.from(document.getElementById('act_multiple_owner').selectedOptions).map(o => o.value)
};
const res = await fetch(`${API}/activities`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
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 {
toastr.error('Failed to create activity.');
}
} catch (err) {
toastr.error("A network error occurred.");
} finally {
// 5. Re-enable Button
// Because this is inside finally{}, it will ALWAYS run after the API call finishes (success or fail)
submitBtn.disabled = false;
submitBtn.innerText = originalBtnText;
}
};
// Activity Timeline - complete Form Submissions
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
const createFollowup = document.getElementById('do_follow').value === 'yes';
let compNotes = document.getElementById('comp_notes').value.trim();
// --- VALIDATION CHECKS (Button is still normal here) ---
if (compNotes === '') {
toastr.warning('Completion notes is required');
return;
}
// FIX: Just check the boolean value directly
if (createFollowup) {
let fNotes = document.getElementById('f_notes').value.trim();
let fDate = document.getElementById('f_date').value.trim();
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 select the next activity type.');
return;
}
// if (!selectedFollowUpActivityType) {
// toastr.warning('Please Pickup Next Activity Type');
// // 1. Get all the buttons
// const buttons = document.querySelectorAll('.f_activity_type');
// // 2. Add the blink class to all buttons
// buttons.forEach(btn => {
// btn.classList.add('blink-error');
// });
// // 3. Remove the class after the animation ends (300ms)
// // so it can be played again later
// setTimeout(() => {
// buttons.forEach(btn => {
// btn.classList.remove('blink-error');
// });
// }, 300);
// return; // Stop the form submission
// }
if (!fNotes) {
toastr.warning('Follow-up notes is required');
return;
}
if (!fDate) {
toastr.warning('Scheduled date & time is required');
return;
}
if (!fAssigned) {
toastr.warning('Please Select the user to be Assigned');
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: createFollowup,
};
// 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) {
// Handle Follow-up logic
if (createFollowup) {
let feFormat = document.getElementById('f_date').value;
let dbFormat = convertDBFormatted(feFormat);
const payload2 = {
lead_id: leadId,
activity_type: selectedFollowUpActivityType,
notes: document.getElementById('f_notes').value,
scheduled_date: dbFormat,
assigned_to: document.getElementById('f_assigned_to').value,
status: 'pending',
additional_assigned_ids : Array.from(document.getElementById('f_multiple_owner').selectedOptions).map(o => o.value)
};
const res2 = await fetch(`${API}/activities`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload2)
});
if (!res2.ok) {
console.error("Follow-up failed to save");
} else {
console.log("Follow-up Success to save");
}
}
toastr.success('Updated Successfully');
// Final Actions
closeModal('completeModal');
viewDetail(leadId);
e.target.reset();
selectedFollowUpActivityType = ''; // Reset global variable
} 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 submitToRedirectwithactualLeadIDUrl(){
let actual_lead_id = document.getElementById('opp_lead_id').value;
let lead_type = $('input[name="lead_form_type"]:checked').val();
// URL: /type / actual_lead_id
let url = '<?=base_url('/util/getLeadNonEB/')?>' + lead_type + '/' + actual_lead_id ;
console.log('lead_type ', lead_type);
console.log('opp_lead_id ', actual_lead_id);
console.log('url ', url);
window.location.href = url;
}
function selectOption(element, radioId) {
// Remove selected class from all options
document.querySelectorAll('.radio-option').forEach(option => {
option.classList.remove('selected');
});
// Add selected class to clicked option
element.classList.add('selected');
// Check the corresponding radio button
document.getElementById(radioId).checked = true;
}
// Change Funcationlity
document.getElementById('do_follow').addEventListener('change', function () {
const isYes = this.value === 'yes';
// 1. Toggle section visibility
document.getElementById('f_up').style.display = isYes ? 'block' : 'none';
// 2. Grab inputs
const notes = document.getElementById('f_notes');
const date = document.getElementById('f_date');
const assigned = document.getElementById('f_assigned_to');
// 3. Toggle properties dynamically using the boolean `isYes`
// If isYes is true: required = true, readOnly = false, disabled = false
[notes, date, assigned].forEach(el => el.required = isYes);
notes.readOnly = date.readOnly = !isYes;
assigned.disabled = !isYes;
// 4. Clear values and reset state if "no"
if (!isYes) {
notes.value = date.value = '';
// Remove active class from buttons
document.querySelectorAll('#f_typeButtons .f_activity_type').forEach(btn => btn.classList.remove('active'));
selectedFollowUpActivityType = ''; // Reset global variable
}
});
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();
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;
trackerEditLeadOriginalCompanyNorm = (lead.company_name || '').trim().toLowerCase();
// 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 || '';
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"]').value = lead.assigned_to || '';
$(form.querySelector('[name="assigned_to"]')).trigger('change');
const statusSelect = form.querySelector('[name="status"]');
statusSelect.value = lead.status || 'New';
updateStatusOptions(statusSelect.value);
// 4. Handle Contact Persons (Looping through the nested array)
const contacts = lead.contact_persons; // Array from your JSON
if (contacts && contacts.length > 0) {
$('#noContactPersonsData').hide();
contacts.forEach(contact => {
const isPrimary = contact.is_primary == 1;
const primaryBadge = isPrimary ? `<div style="margin-top:2px;"><span class="badge bg-warning text-dark ml-1" style="font-size:9px;">PRIMARY</span></div>` : '';
let contactHtml = `
<div class="contact-card d-flex align-items-center mb-2 p-2 px-3"
style="background: ${isPrimary ? '#fff9c4' : '#f8f8f8'}; border: 1px solid ${isPrimary ? '#fbc02d' : '#eee'}; border-radius: 8px; gap: 12px;">
<div style="flex: 2; min-width: 0;">
<div class="fw-bold text-truncate" style="font-size:14px;">${contact.name} ${primaryBadge}</div>
</div>
<div style="flex: 2; min-width: 0; color:#666; font-size:13px;">${contact.mobile || '-'}</div>
<div style="flex: 2; min-width: 0; color:#666; font-size:13px;"><div class="text-truncate">${contact.designation || '-'}</div></div>
<div class="d-flex" style="gap:5px;">
<button type="button" class="btnEditContact" data-id="${contact.contact_id}" data-info='${JSON.stringify(contact)}' title="Edit Contact Person"
style="background:none; border:none; cursor:pointer; font-size:16px; flex-shrink:0; padding:4px 6px; border-radius:6px; transition:background 0.2s;"
onmouseover="this.style.background='#e3f2fd'; this.querySelector('i').style.color='#1976d2';"
onmouseout="this.style.background='none'; this.querySelector('i').style.color='#555';">
<i class="ri-pencil-fill" style="color:#555;"></i>
</button>
<button type="button" class="btnRemoveContact" data-id="${contact.contact_id}" title="Remove Contact Person"
style="background:none; border:none; cursor:pointer; font-size:16px; flex-shrink:0; padding:4px 6px; border-radius:6px; transition:background 0.2s;"
onmouseover="this.style.background='#ffebee'; this.querySelector('i').style.color='#d32f2f';"
onmouseout="this.style.background='none'; this.querySelector('i').style.color='#555';">
<i class="ri-delete-bin-fill" style="color:#555;"></i>
</button>
</div>
</div>
`;
container.append(contactHtml);
});
}
else {
$('#noContactPersonsData').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.");
}
}
// --- 1. HANDLE EDIT & REMOVE (Event Delegation) ---
document.getElementById('contactPersonsList').onclick = async (e) => {
const btnEdit = e.target.closest('.btnEditContact');
const btnRemove = e.target.closest('.btnRemoveContact');
// EDIT LOGIC
if (btnEdit) {
const card = btnEdit.closest('.contact-card');
const contactData = JSON.parse(btnEdit.dataset.info);
// Fill Form
document.getElementById('editing_contact_id').value = contactData.contact_id;
document.getElementById('contact_name').value = contactData.name;
document.getElementById('contact_mobile').value = contactData.mobile;
document.getElementById('contact_designation').value = contactData.designation;
document.getElementById('contact_is_primary').checked = contactData.is_primary == 1;
// Change Button UI
document.getElementById('btnSaveContact').innerHTML = "<i class='ri-pencil-fill'></i>";
document.getElementById('btnSaveContact').title = "Update";
document.getElementById('btnSaveContact').style.background = "#02a8b5";
document.getElementById('contact_name').focus();
}
// REMOVE LOGIC
if (btnRemove) {
const contactId = btnRemove.dataset.id;
if (!confirm('Are you sure you want to remove this contact?')) return;
try {
const res = await fetch(`${API}/contacts/${contactId}`, { method: 'DELETE' });
if (res.ok) {
toastr.success('Removed successfully');
openEditLeadModal(document.getElementById('hidden_lead_id').value); // Refresh
}
} catch (err) { console.error(err); }
}
};
// --- 2. SAVE / UPDATE LOGIC ---
const btnSaveContact = document.getElementById('btnSaveContact');
if (btnSaveContact) {
btnSaveContact.onclick = async (e) => {
const editingId = document.getElementById('editing_contact_id').value;
const leadId = document.getElementById('hidden_lead_id').value;
let payload = {
lead_id: leadId,
name: document.getElementById('contact_name').value.trim(),
mobile: document.getElementById('contact_mobile').value.trim(),
designation: document.getElementById('contact_designation').value.trim(),
is_primary: document.getElementById('contact_is_primary').checked ? 1 : 0
};
if (!validateContactForm()) {
return toastr.warning('Please correct highlighted fields.', 'Validation Error');
}
// Determine if we POST (new) or PUT (edit)
const url = editingId ? `${API}/contacts/${editingId}` : `${API}/contacts`;
const method = editingId ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (res.ok) {
toastr.success(editingId ? 'Contact Updated' : 'Contact Saved');
// Reset Form UI
resetContactForm();
// Refresh List
openEditLeadModal(leadId);
} else {
const err = await res.json();
if (err.messages?.name) setContactFieldError('name', err.messages.name);
if (err.messages?.mobile) setContactFieldError('mobile', err.messages.mobile);
toastr.error(err.message || 'Error processing request');
}
} catch (error) { console.error(error); }
};
}
const btnClearContact = document.getElementById('btnClearContact');
if (btnClearContact) {
btnClearContact.onclick = () => resetContactForm();
}
const statusOrder = ['New', 'Potential', 'Prospects', 'Not a Prospects'];
function updateStatusOptions(currentStatus) {
let Eform = document.getElementById('editLeadForm');
if (!Eform) return; // safety check
let select = Eform.querySelector('[name="status"]');
let currentIndex = statusOrder.indexOf(currentStatus);
// First show all
select.querySelectorAll('option').forEach(opt => {
opt.hidden = false;
});
// Hide previous ones
if (currentIndex > -1) {
statusOrder.slice(0, currentIndex).forEach(status => {
const opt = select.querySelector(`option[value="${status}"]`);
if (opt) opt.hidden = true;
});
}
}
function convertDBFormatted(input) {
if (!input) return null;
let parts = input.trim().split(' ');
if (parts.length !== 3) return null;
let [datePart, timePart, ampm] = parts;
let [day, month, year] = datePart.split('/');
let [hours, minutes, seconds] = timePart.split(':');
hours = parseInt(hours, 10);
// Convert 12-hour to 24-hour
if (ampm.toUpperCase() === "PM" && hours < 12) hours += 12;
if (ampm.toUpperCase() === "AM" && hours === 12) hours = 0;
return (
year + '-' +
month.padStart(2, '0') + '-' +
day.padStart(2, '0') + ' ' +
String(hours).padStart(2, '0') + ':' +
minutes.padStart(2, '0') + ':' +
seconds.padStart(2, '0')
);
}
function csvEscape(value) {
const text = String(value ?? '').replace(/"/g, '""');
return `"${text}"`;
}
function capitalizeStatus(value) {
const text = String(value ?? '').trim();
return text ? text.charAt(0).toUpperCase() + text.slice(1).toLowerCase() : '';
}
function formatIndianDate(value) {
if (!value) return '';
const date = new Date(String(value).replace(' ', 'T'));
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString('en-IN', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
}).replace(',', '').toUpperCase();
}
function downloadCsv(filename, rows) {
const csv = rows.map(row => row.map(csvEscape).join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
async function exportLeadsCsv() {
const selectedMemberIds = getSelectedMemberIds(salesManagerHeadIds);
if (selectedMemberIds.length === 0) return toastr.warning('No members available to export');
const params = new URLSearchParams({
status: filter === 'all' ? '' : filter,
search: document.getElementById('mainSearch')?.value || '',
limit: 10000,
offset: 0,
assigned_to: selectedMemberIds.join(','),
financial_year: getSelectedFinancialYear()
});
const res = await fetch(`${API}/leads?${params.toString()}`);
const json = await res.json();
const rows = [
['Company', 'Email', 'Phone', 'Status', 'Assigned To', 'Created At'],
...(json.data || []).map(l => [
l.company_name || '',
l.email || '',
l.phone || '',
capitalizeStatus(l.status),
l.assigned_to_name || '',
formatIndianDate(l.created_at)
])
];
downloadCsv(`sales-leads-${getSelectedFinancialYear()}.csv`, rows);
}
initSalesToolbarFilters();
fetchLeads().then(() => {
const leadId = new URLSearchParams(window.location.search).get('lead_id');
if (leadId) {
viewDetail(leadId);
}
});
// ================================================================
// 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 ? `&nbsp;${client.email}` : ''}
// ${client.phone ? `&nbsp;· ${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 &nbsp;<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>