2362 lines
113 KiB
PHP
2362 lines
113 KiB
PHP
<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 20px; border-radius: 20px; cursor: pointer; font-size: 14px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; }
|
||
.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: 10px; }
|
||
.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">
|
||
|
||
<!-- 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)" style="width: 300px !important;">
|
||
<button class="btn-primary" onclick="openModal('addLeadModal')">
|
||
+ Add Lead
|
||
</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 class="parsley-examples" id="addLeadForm" enctype="multipart/form-data">
|
||
<hr class="my-0">
|
||
<div class="modal-body p-4">
|
||
<div class="form-group">
|
||
<div class="col-12 mb-1">
|
||
<div class="col-xl-12 col-lg-12 col-md-12" style="position:relative;">
|
||
|
||
<!-- Label with inline short name badge -->
|
||
<label class="form-label">
|
||
Company Name<span class="text-danger">*</span>
|
||
|
||
<span id="shortNameBadge" style="display:none; background:#FAEEDA; color:#854F0B;
|
||
padding:2px 10px; border-radius:999px; font-size:11px; font-weight:600;
|
||
letter-spacing:0.5px; vertical-align:middle;">
|
||
</span>
|
||
</label>
|
||
|
||
<!-- Hidden fields -->
|
||
<input type="hidden" name="client_id" id="lead_client_id" value="">
|
||
<input type="hidden" name="short_name" id="lead_short_name_hidden" value="">
|
||
|
||
|
||
<!-- Selected pill (shown after picking existing client) -->
|
||
<div id="companySelected" style="display:none; align-items:center; gap:8px;
|
||
border:1px solid #B5D4F4; border-radius:6px; padding:6px 12px; background:#E6F1FB;">
|
||
<span id="companySelectedName" style="flex:1; font-size:14px; font-weight:500;"></span>
|
||
<span onclick="clearCompanySelection('add')" style="cursor:pointer; color:#888; font-size:18px; line-height:1;">×</span>
|
||
</div>
|
||
|
||
<!-- Search input -->
|
||
<input type="text" id="companySearchInput" class="form-control"
|
||
name="company_name" placeholder="Search or create company..."
|
||
autocomplete="off" oninput="searchCompany(this.value, 'add')" required
|
||
style="width:100%;">
|
||
|
||
<!-- Dropdown -->
|
||
<div id="companyDropdown" style="display:none; position:absolute; z-index:9999;
|
||
width:100%; background:#fff; border:1px solid #ddd; border-radius:6px;
|
||
box-shadow:0 4px 12px rgba(0,0,0,0.1); max-height:220px; overflow-y:auto;
|
||
top:100%; left:0;">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-row mb-1">
|
||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||
<label class="form-label">Email</label>
|
||
<input type="email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\-_+]/g, '')">
|
||
</div>
|
||
<div class="col-md-12" style="width: 94%;">
|
||
<label class="form-label">Mobile</label>
|
||
<!-- <input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')"> -->
|
||
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10);">
|
||
</div>
|
||
</div>
|
||
<div class="form-row mb-1">
|
||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||
<label class="form-label">Status<span class="text-danger">*</span></label>
|
||
<select name="status" class="form-control" style="width: 100%;" required>
|
||
<option value="New">New</option>
|
||
<option value="Potential">Potential</option>
|
||
<option value="Prospects">Prospects</option>
|
||
<option value="Not a Prospects">Not a Prospects</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-12" style="width: 94%;">
|
||
<label class="form-label">Assign To<span class="text-danger">*</span></label>
|
||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||
<option value="">Select User</option>
|
||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="form-group text-right mb-0">
|
||
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('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 class="parsley-examples" id="editLeadForm" enctype="multipart/form-data">
|
||
<hr class="my-0">
|
||
<input type="hidden" class="form-control" id="hidden_lead_id" name="lead_id">
|
||
<div class="modal-body p-4">
|
||
<div class="form-group">
|
||
<div class="col-12 mb-1">
|
||
<!-- <div class="col-xl-12 col-lg-12 col-md-12">
|
||
<label class="form-label">Company Name <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" name="company_name" style="width: 100%;" placeholder="Enter Company Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')" required>
|
||
</div> -->
|
||
<div class="col-xl-12 col-lg-12 col-md-12" style="position:relative;">
|
||
|
||
<!-- Label with inline short name badge -->
|
||
<label class="form-label">
|
||
Company Name <span class="text-danger">*</span>
|
||
|
||
<span id="editShortNameBadge" style="display:none; background:#FAEEDA; color:#854F0B;
|
||
padding:2px 10px; border-radius:999px; font-size:11px; font-weight:600;
|
||
letter-spacing:0.5px; vertical-align:middle;">
|
||
</span>
|
||
</label>
|
||
|
||
<!-- Hidden fields -->
|
||
<input type="hidden" name="client_id" id="edit_lead_client_id" value="">
|
||
<input type="hidden" name="short_name" id="edit_lead_short_name_hidden" value="">
|
||
|
||
<!-- Selected pill (shown after picking existing client) -->
|
||
<div id="editCompanySelected" style="display:none; align-items:center; gap:8px;
|
||
border:1px solid #B5D4F4; border-radius:6px; padding:6px 12px; background:#E6F1FB;">
|
||
<span id="editCompanySelectedName" style="flex:1; font-size:14px; font-weight:500;"></span>
|
||
<span onclick="clearCompanySelection('edit')" style="cursor:pointer; color:#888; font-size:18px; line-height:1;">×</span>
|
||
</div>
|
||
|
||
<!-- Search input -->
|
||
<input type="text" id="editCompanySearchInput" class="form-control"
|
||
name="company_name" placeholder="Search or create company..."
|
||
autocomplete="off" oninput="searchCompany(this.value, 'edit')" required
|
||
style="width:100%;">
|
||
|
||
<!-- Dropdown -->
|
||
<div id="editCompanyDropdown" style="display:none; position:absolute; z-index:9999;
|
||
width:100%; background:#fff; border:1px solid #ddd; border-radius:6px;
|
||
box-shadow:0 4px 12px rgba(0,0,0,0.1); max-height:220px; overflow-y:auto;
|
||
top:100%; left:0;">
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
<div class="form-row mb-1">
|
||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||
<label class="form-label">Email</label>
|
||
<input type="email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\-_+]/g, '')">
|
||
</div>
|
||
<div class="col-md-12" style="width: 94%;">
|
||
<label class="form-label">Mobile</label>
|
||
<!-- <input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')"> -->
|
||
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10);">
|
||
</div>
|
||
</div>
|
||
<div class="col-12 mb-1">
|
||
<div class="col-xl-12 col-lg-12 col-md-12">
|
||
<label class="form-label">Address</label>
|
||
<textarea id="address" name="address" class="form-control" style="width:100%; height:100px;" placeholder="Enter full Address"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="form-row mb-1">
|
||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||
<label class="form-label">WebSite</label>
|
||
<input type="text" class="form-control" name="website" style="width: 100%;" placeholder="Enter the Website">
|
||
</div>
|
||
<div class="col-md-12" style="width: 94%;">
|
||
<label class="form-label">GST Number</label>
|
||
<input type="text" class="form-control" name="gst_number" style="width: 100%;" placeholder="Enter the GST Number" oninput="this.value = this.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()" maxlength="15"
|
||
pattern="^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$">
|
||
</div>
|
||
</div>
|
||
|
||
<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, '')">
|
||
</div>
|
||
|
||
<div class="col-md-3 col-6">
|
||
<label class="form-label">Mobile <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control form-control-sm" id="contact_mobile" placeholder="Mobile" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10);">
|
||
</div>
|
||
|
||
<div class="col-md-3 col-6">
|
||
<label class="form-label">Designation </label>
|
||
<input type="text" class="form-control form-control-sm" id="contact_designation" placeholder="Designation" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')">
|
||
</div>
|
||
|
||
<div class="col-md-1 col-3">
|
||
<label class="form-label"> </label>
|
||
<div class="primary-container">
|
||
<label for="contact_is_primary" class="primary-label">Primary</label>
|
||
<input class="form-check-input" type="checkbox" id="contact_is_primary">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- <div class="col-md-2 col-6">
|
||
<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 name="status" class="form-control" style="width: 100%;" required>
|
||
<option value="New">New</option>
|
||
<option value="Potential">Potential</option>
|
||
<option value="Prospects">Prospects</option>
|
||
<option value="Not a Prospects">Not a Prospects</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-12" style="width: 94%;">
|
||
<label class="form-label">Assign To <span class="text-danger">*</span> </label>
|
||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||
<option value="">Select User</option>
|
||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="form-group text-right mb-0">
|
||
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('editLeadModal')">Cancel</button>
|
||
<button type="submit" class="btn-primary">Update Lead</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
resetFlatpicker();
|
||
|
||
// Single selects
|
||
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||
let parentModal = el.closest('.modal');
|
||
$(el).select2({
|
||
placeholder: "Select..",
|
||
allowClear: false,
|
||
minimumResultsForSearch: 0,
|
||
dropdownParent: parentModal ? $(parentModal) : $(document.body)
|
||
});
|
||
});
|
||
|
||
// Multi selects
|
||
document.querySelectorAll('.multi-searchable').forEach(function(el) {
|
||
let parentModal = el.closest('.modal');
|
||
$(el).select2({
|
||
placeholder: "Search and select members...",
|
||
allowClear: false,
|
||
closeOnSelect: false,
|
||
minimumResultsForSearch: 0,
|
||
dropdownParent: parentModal ? $(parentModal) : $(document.body),
|
||
width: '100%'
|
||
});
|
||
});
|
||
});
|
||
|
||
function resetFlatpicker(){
|
||
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') ?>';
|
||
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 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 === '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;
|
||
document.getElementById('btnSaveContact').innerHTML = "✔";
|
||
document.getElementById('btnSaveContact').title = "Save";
|
||
document.getElementById('btnSaveContact').style.background = "#02a8b5";
|
||
}
|
||
// Helper — add this once anywhere in your JS
|
||
function _resetCompanyUI(mode) {
|
||
const ctx = COMPANY_CTX[mode];
|
||
const state = companyState[mode];
|
||
state.isExisting = false;
|
||
state.selectedValue = '';
|
||
document.getElementById(ctx.clientId).value = '';
|
||
document.getElementById(ctx.shortNameHidden).value = ''; // ← only this
|
||
document.getElementById(ctx.selected).style.display = 'none';
|
||
document.getElementById(ctx.searchInput).style.display = '';
|
||
document.getElementById(ctx.searchInput).value = '';
|
||
document.getElementById(ctx.dropdown).style.display = 'none';
|
||
const b = document.getElementById(ctx.badge);
|
||
if (b) { b.textContent = ''; b.style.display = 'none'; }
|
||
}
|
||
|
||
|
||
function selectFollowUpActivityType(val, el) {
|
||
document.querySelectorAll('.f_activity_type').forEach(b => b.classList.remove('active'));
|
||
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;
|
||
fetchLeads();
|
||
}
|
||
|
||
// 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');
|
||
|
||
// 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 (typeof salesManagerHeadIds === 'undefined' || salesManagerHeadIds.length === 0) {
|
||
console.log("No Sales Manager IDs found. Skipping API call.");
|
||
grid.innerHTML = emptyStateHTML;
|
||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||
|
||
// 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
|
||
let url = `${API}/leads?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
|
||
// url += `&assigned_to=${salesManagerHeadIds.join(',')}`; // We already proved it exists above!
|
||
if (typeof salesManagerHeadIds !== 'undefined' && salesManagerHeadIds.length > 0) {
|
||
url += `&assigned_to=${salesManagerHeadIds.join(',')}`;
|
||
}
|
||
|
||
console.log("Fetching API:", url);
|
||
|
||
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');
|
||
}
|
||
|
||
|
||
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
|
||
document.getElementById('addLeadForm').onsubmit = async (e) => {
|
||
e.preventDefault();
|
||
|
||
// Just grab the button variables first, DO NOT disable yet
|
||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||
const originalBtnText = submitBtn.innerText;
|
||
|
||
const data = Object.fromEntries(new FormData(e.target).entries());
|
||
|
||
let companyName = data.company_name?.trim();
|
||
let email = data.email?.trim();
|
||
let phone = data.phone?.trim();
|
||
|
||
let companyRegex = /^[A-Za-z\s]+$/;
|
||
let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
// let phoneRegex = /^\+?[0-9\s]{0,10}$/; // Phone regex (+, numbers, spaces allowed, 10-20 length)
|
||
let phoneRegex = /^\d{10}$/;
|
||
|
||
let addState = companyState['add']; // use 'edit' in editLeadForm
|
||
let modeState = companyState['add']; // ← change to 'edit' for edit form
|
||
|
||
|
||
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
|
||
function showError(message, fieldName) {
|
||
toastr.warning(message, 'Validation Error');
|
||
setTimeout(function() {
|
||
$('[name="' + fieldName + '"]').focus();
|
||
}, 100);
|
||
}
|
||
|
||
if (!modeState.selectedValue && !data.client_id) {
|
||
return showError('Please select a company from the dropdown or click "Create" to add a new one.', 'company_name');
|
||
}
|
||
if (!companyState['add'].selectedValue) {
|
||
return showError('Please select or create a company from the dropdown.', 'company_name');
|
||
}
|
||
|
||
// --- VALIDATION CHECKS (Button is still normal here) ---
|
||
if (!companyName && !email && !phone && !data.assigned_to) {
|
||
toastr.warning('Please fill in all the required fields.');
|
||
return;
|
||
}
|
||
|
||
if (!companyName) {
|
||
return showError('Company Name is required.', 'company_name');
|
||
}
|
||
|
||
if (!companyRegex.test(companyName)) {
|
||
return showError('Company Name can contain only letters and spaces.', 'company_name');
|
||
}
|
||
|
||
if (companyName.length < 2) {
|
||
return showError('Company Name must be at least 2 characters.', 'company_name');
|
||
}
|
||
|
||
if (!email && !phone) {
|
||
return showError("Either Email or Phone number is required.", 'email');
|
||
}
|
||
|
||
if (email && !emailRegex.test(email)) {
|
||
return showError("Please enter a valid email address.", 'email');
|
||
}
|
||
|
||
if (phone && !phoneRegex.test(phone)) {
|
||
// return showError("Phone number can contain only +, numbers and spaces (10–20 digits).", 'phone');
|
||
return showError("Phone number can contain only numbers (10 digits)", 'phone');
|
||
}
|
||
|
||
if (!data.status) {
|
||
return showError("Please select a status.", 'status');
|
||
}
|
||
|
||
if (!data.assigned_to) {
|
||
return showError("Please Select the user to be Assigned.", 'assigned_to');
|
||
}
|
||
|
||
// --- ALL VALIDATION PASSED: Now disable button ---
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerText = 'Creating...';
|
||
|
||
try {
|
||
const res = await fetch(`${API}/leads`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
if(res.ok) {
|
||
toastr.success('Leads Created Successfully');
|
||
closeModal('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]) => {
|
||
let inputElement = $('[name="' + field + '"]');
|
||
|
||
if (inputElement.length > 0) {
|
||
inputElement.addClass('is-invalid');
|
||
if (isFirstError) {
|
||
setTimeout(function() { inputElement.focus(); }, 100);
|
||
isFirstError = false;
|
||
}
|
||
}
|
||
if (!seenMessages.includes(message)) {
|
||
errorMessages += `• ${message}<br>`;
|
||
seenMessages.push(message);
|
||
}
|
||
});
|
||
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
|
||
} else {
|
||
toastr.warning(err.message || 'Validation failed', 'Warning');
|
||
}
|
||
} else {
|
||
toastr.error(err.message || 'Error adding lead');
|
||
}
|
||
}
|
||
} catch (err) {
|
||
toastr.error("A network error occurred.");
|
||
} finally {
|
||
// ALWAYS runs to reset button
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerText = originalBtnText;
|
||
}
|
||
};
|
||
|
||
|
||
|
||
// Edit Lead Form Submissions
|
||
document.getElementById('editLeadForm').onsubmit = async (e) => {
|
||
e.preventDefault();
|
||
|
||
// Just grab the button variables first, DO NOT disable yet
|
||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||
const originalBtnText = submitBtn.innerText;
|
||
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();
|
||
let email = data.email?.trim();
|
||
let phone = data.phone?.trim();
|
||
let gst = data.gst_number?.trim();
|
||
|
||
let companyRegex = /^[A-Za-z\s]+$/;
|
||
let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
// let phoneRegex = /^\+?[0-9\s]{10,20}$/;
|
||
let phoneRegex = /^\d{10}$/;
|
||
let gstRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/;
|
||
|
||
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
|
||
function showError(message, fieldName) {
|
||
toastr.warning(message, 'Validation Error');
|
||
setTimeout(function() {
|
||
$('[name="' + fieldName + '"]').focus();
|
||
}, 100);
|
||
}
|
||
|
||
// --- VALIDATION CHECKS (Button is still normal here) ---
|
||
if (!modeState.selectedValue && !data.client_id) {
|
||
return showError('Please select a company from the dropdown or click "Create" to add a new one.', 'company_name');
|
||
}
|
||
|
||
if (!companyState['edit'].selectedValue) {
|
||
return showError('Please select or create a company from the dropdown.', 'company_name');
|
||
}
|
||
|
||
if (!companyName && !email && !phone && !data.assigned_to) {
|
||
toastr.warning('Please fill in all the required fields.');
|
||
return;
|
||
}
|
||
|
||
if (!companyName) {
|
||
return showError('Company Name is required.', 'company_name');
|
||
}
|
||
|
||
if (!companyRegex.test(companyName)) {
|
||
return showError('Company Name can contain only letters and spaces.', 'company_name');
|
||
}
|
||
|
||
if (companyName.length < 2) {
|
||
return showError('Company Name must be at least 2 characters.', 'company_name');
|
||
}
|
||
|
||
if (!email && !phone) {
|
||
return showError("Either Email or Phone number is required.", 'email');
|
||
}
|
||
|
||
if (email && !emailRegex.test(email)) {
|
||
return showError("Please enter a valid email address.", 'email');
|
||
}
|
||
|
||
if (phone && !phoneRegex.test(phone)) {
|
||
// return showError("Phone number can contain only +, numbers and spaces (10–20 digits).", 'phone');
|
||
return showError("Phone number can contain only numbers (10 digits)", 'phone');
|
||
}
|
||
|
||
if (gst && !gstRegex.test(gst)) {
|
||
return showError("Please enter a valid GST Number (e.g., 22AAAAA0000A1Z5).", 'gst_number');
|
||
}
|
||
|
||
if (!data.status) {
|
||
return showError("Please select a status.", 'status');
|
||
}
|
||
|
||
if (!data.assigned_to) {
|
||
return showError("Please Select the user to be Assigned.", 'assigned_to');
|
||
}
|
||
|
||
// --- ALL VALIDATION PASSED: Now disable button ---
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerText = 'Updating...';
|
||
|
||
try {
|
||
const res = await fetch(`${API}/leads/${leadId}`, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if(res.ok) {
|
||
toastr.success('Leads Updated Successfully');
|
||
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]) => {
|
||
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;
|
||
|
||
// 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 (!payload.name || !payload.mobile) {
|
||
return toastr.warning('Name and Mobile are required');
|
||
}
|
||
|
||
// Determine if we POST (new) or PUT (edit)
|
||
const url = editingId ? `${API}/contacts/${editingId}` : `${API}/contacts`;
|
||
const method = editingId ? 'PUT' : 'POST';
|
||
|
||
try {
|
||
const res = await fetch(url, {
|
||
method: method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
if (res.ok) {
|
||
toastr.success(editingId ? 'Contact Updated' : 'Contact Saved');
|
||
|
||
// Reset Form UI
|
||
resetContactForm();
|
||
|
||
// Refresh List
|
||
openEditLeadModal(leadId);
|
||
} else {
|
||
const err = await res.json();
|
||
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')
|
||
);
|
||
}
|
||
fetchLeads();
|
||
|
||
// ================================================================
|
||
// COMPANY SEARCH — Odoo-style autocomplete
|
||
// UNIFIED COMPANY SEARCH — works for both Add & Edit modals
|
||
// ================================================================
|
||
|
||
// Context config for each modal
|
||
const COMPANY_CTX = {
|
||
add: {
|
||
searchInput: 'companySearchInput',
|
||
dropdown: 'companyDropdown',
|
||
selected: 'companySelected',
|
||
selectedName: 'companySelectedName',
|
||
badge: 'shortNameBadge',
|
||
clientId: 'lead_client_id',
|
||
shortNameHidden:'lead_short_name_hidden',
|
||
formId: 'addLeadForm',
|
||
},
|
||
edit: {
|
||
searchInput: 'editCompanySearchInput',
|
||
dropdown: 'editCompanyDropdown',
|
||
selected: 'editCompanySelected',
|
||
selectedName: 'editCompanySelectedName',
|
||
badge: 'editShortNameBadge',
|
||
clientId: 'edit_lead_client_id',
|
||
shortNameHidden:'edit_lead_short_name_hidden',
|
||
formId: 'editLeadForm',
|
||
}
|
||
};
|
||
|
||
// Track state per modal
|
||
const companyState = { add: { isExisting: false, selectedValue: '' },
|
||
edit: { isExisting: false, selectedValue: '' } };
|
||
|
||
// Timer per modal
|
||
const companyTimers = { add: null, edit: null };
|
||
|
||
// ── Called from oninput on both search inputs ─────────────────────
|
||
function searchCompany(val, mode) {
|
||
const ctx = COMPANY_CTX[mode];
|
||
const state = companyState[mode];
|
||
clearTimeout(companyTimers[mode]);
|
||
|
||
const dropdown = document.getElementById(ctx.dropdown);
|
||
const badge = document.getElementById(ctx.badge);
|
||
|
||
if (!val || val.trim().length < 1) {
|
||
dropdown.style.display = 'none';
|
||
badge.textContent = '';
|
||
badge.style.display = 'none';
|
||
document.getElementById(ctx.shortNameHidden).value = '';
|
||
document.getElementById(ctx.shortNameField).value = '';
|
||
state.selectedValue = ''; // ← clear selected value tracking
|
||
return;
|
||
}
|
||
|
||
// Only generate short name for new clients while typing
|
||
if (!state.isExisting) {
|
||
generateAndSetShortName(val, mode);
|
||
}
|
||
|
||
companyTimers[mode] = setTimeout(async () => {
|
||
try {
|
||
const res = await fetch(`<?= base_url('sales/searchClients') ?>?q=${encodeURIComponent(val)}`);
|
||
const json = await res.json();
|
||
renderCompanyDropdown(json.data || [], val, mode);
|
||
} catch (e) {
|
||
renderCompanyDropdown([], val, mode);
|
||
}
|
||
}, 250);
|
||
}
|
||
|
||
function renderCompanyDropdown(results, query, mode) {
|
||
const ctx = COMPANY_CTX[mode];
|
||
const dropdown = document.getElementById(ctx.dropdown);
|
||
dropdown.innerHTML = '';
|
||
|
||
// ── Existing matches ──────────────────────────────────────────
|
||
results.forEach(client => {
|
||
const safe = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
const highlighted = client.client_name.replace(
|
||
new RegExp(`(${safe})`, 'gi'),
|
||
'<strong style="color:#185FA5;">$1</strong>'
|
||
);
|
||
const initials = client.client_name.substring(0, 2).toUpperCase();
|
||
|
||
const item = document.createElement('div');
|
||
item.style.cssText = 'display:flex;align-items:center;gap:10px;padding:9px 14px;cursor:pointer;font-size:13px;border-bottom:1px solid #f5f5f5;';
|
||
item.innerHTML = `
|
||
<div style="width:32px;height:32px;border-radius:50%;background:#B5D4F4;display:flex;
|
||
align-items:center;justify-content:center;font-size:11px;font-weight:600;
|
||
color:#0C447C;flex-shrink:0;">${initials}</div>
|
||
<div style="flex:1;min-width:0;">
|
||
<div style="font-weight:500;">${highlighted}</div>
|
||
<div style="font-size:11px;color:#aaa;margin-top:1px;">
|
||
<span style="background:#e8f4fd;color:#185FA5;padding:1px 6px;border-radius:4px;font-size:10px;">${client.short_name || 'N/A'}</span>
|
||
${client.email ? ` ${client.email}` : ''}
|
||
${client.phone ? ` · ${client.phone}` : ''}
|
||
</div>
|
||
</div>`;
|
||
item.onmouseenter = () => item.style.background = '#f7f9fc';
|
||
item.onmouseleave = () => item.style.background = '';
|
||
item.onclick = () => selectExistingCompany(client, mode);
|
||
dropdown.appendChild(item);
|
||
});
|
||
|
||
// ── Divider ───────────────────────────────────────────────────
|
||
if (results.length > 0) {
|
||
const div = document.createElement('div');
|
||
div.style.cssText = 'height:1px;background:#eee;margin:4px 0;';
|
||
dropdown.appendChild(div);
|
||
}
|
||
|
||
// ── Create new option ─────────────────────────────────────────
|
||
const createItem = document.createElement('div');
|
||
createItem.style.cssText = 'display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;font-size:13px;color:#185FA5;font-weight:500;';
|
||
createItem.innerHTML = `
|
||
<span style="background:#E6F1FB;border-radius:4px;padding:2px 8px;font-size:11px;font-weight:700;">+</span>
|
||
Create <strong>"${query}"</strong>`;
|
||
createItem.onmouseenter = () => createItem.style.background = '#f0f7ff';
|
||
createItem.onmouseleave = () => createItem.style.background = '';
|
||
createItem.onclick = () => selectNewCompany(query, mode);
|
||
dropdown.appendChild(createItem);
|
||
|
||
dropdown.style.display = 'block';
|
||
}
|
||
|
||
// ── User picked an EXISTING client from dropdown ──────────────────
|
||
function selectExistingCompany(client, mode) {
|
||
const ctx = COMPANY_CTX[mode];
|
||
const state = companyState[mode];
|
||
state.isExisting = true;
|
||
state.selectedValue = client.client_name;
|
||
|
||
document.getElementById(ctx.clientId).value = client.id;
|
||
document.getElementById(ctx.shortNameHidden).value = client.short_name || ''; // ← only this
|
||
|
||
const searchInp = document.getElementById(ctx.searchInput);
|
||
searchInp.value = client.client_name;
|
||
searchInp.style.display = 'none';
|
||
document.getElementById(ctx.selectedName).textContent = client.client_name;
|
||
document.getElementById(ctx.selected).style.display = 'flex';
|
||
document.getElementById(ctx.dropdown).style.display = 'none';
|
||
|
||
const badge = document.getElementById(ctx.badge);
|
||
badge.textContent = client.short_name || '';
|
||
badge.style.cssText = 'display:inline-block;background:#E6F1FB;color:#185FA5;' +
|
||
'padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600;' +
|
||
'letter-spacing:0.5px;vertical-align:middle;';
|
||
|
||
const form = document.getElementById(ctx.formId);
|
||
const ef = form.querySelector('[name="email"]');
|
||
const pf = form.querySelector('[name="phone"]');
|
||
if (ef && client.email && !ef.value) ef.value = client.email;
|
||
if (pf && client.phone && !pf.value) pf.value = client.phone;
|
||
}
|
||
|
||
// ── User clicked "Create new" ─────────────────────────────────────
|
||
async function selectNewCompany(name, mode) {
|
||
// ── Duplicate check ──────────────────────────────────
|
||
try {
|
||
const res = await fetch(`<?= base_url('sales/checkDuplicate') ?>?table=clients&field=client_name&value=${encodeURIComponent(name)}`);
|
||
const json = await res.json();
|
||
if (json.exists) {
|
||
toastr.warning(`"${name}" already exists. Search and select it from the dropdown instead.`);
|
||
document.getElementById(COMPANY_CTX[mode].searchInput).value = '';
|
||
document.getElementById(COMPANY_CTX[mode].searchInput).focus();
|
||
return;
|
||
}
|
||
} catch(e) {
|
||
// silently allow on network error — server will catch it
|
||
}
|
||
// ── Rest of your existing code (unchanged) ───────────
|
||
const ctx = COMPANY_CTX[mode];
|
||
const state = companyState[mode];
|
||
state.isExisting = false;
|
||
state.selectedValue = name; // ← track what was confirmed as new
|
||
document.getElementById(ctx.clientId).value = '';
|
||
const inp = document.getElementById(ctx.searchInput);
|
||
inp.value = name;
|
||
document.getElementById(ctx.dropdown).style.display = 'none';
|
||
generateAndSetShortName(name, mode);
|
||
|
||
}
|
||
|
||
// ── × button on pill ─────────────────────────────────────────────
|
||
function clearCompanySelection(mode) {
|
||
const ctx = COMPANY_CTX[mode];
|
||
const state = companyState[mode];
|
||
state.isExisting = false;
|
||
state.selectedValue = '';
|
||
|
||
document.getElementById(ctx.clientId).value = '';
|
||
document.getElementById(ctx.shortNameHidden).value = ''; // ← only this
|
||
document.getElementById(ctx.selected).style.display = 'none';
|
||
|
||
const badge = document.getElementById(ctx.badge);
|
||
badge.textContent = '';
|
||
badge.style.display = 'none';
|
||
|
||
const inp = document.getElementById(ctx.searchInput);
|
||
inp.style.display = '';
|
||
inp.value = '';
|
||
inp.focus();
|
||
|
||
if (mode === 'add') {
|
||
const form = document.getElementById(ctx.formId);
|
||
const ef = form.querySelector('[name="email"]');
|
||
const pf = form.querySelector('[name="phone"]');
|
||
if (ef) ef.value = '';
|
||
if (pf) pf.value = '';
|
||
}
|
||
}
|
||
|
||
// ── Short name generation (shared) ───────────────────────────────
|
||
function generateAndSetShortName(name, mode) {
|
||
// FIX 2.2: trim ALL spaces before generating
|
||
const base = name.trim().replace(/\s+/g, '').substring(0, 10).toUpperCase();
|
||
if (!base) return;
|
||
|
||
checkShortNameInBothTables(base, (isDupe) => {
|
||
if (!isDupe) {
|
||
setShortName(base, mode);
|
||
} else {
|
||
let counter = 1;
|
||
const tryNext = () => {
|
||
const candidate = base.substring(0, 8) + String(counter).padStart(2, '0');
|
||
checkShortNameInBothTables(candidate, (exists) => {
|
||
if (exists) { counter++; tryNext(); }
|
||
else { setShortName(candidate, mode); }
|
||
});
|
||
};
|
||
tryNext();
|
||
}
|
||
});
|
||
}
|
||
|
||
function setShortName(val, mode) {
|
||
const ctx = COMPANY_CTX[mode];
|
||
document.getElementById(ctx.shortNameHidden).value = val; // ← only this
|
||
|
||
const badge = document.getElementById(ctx.badge);
|
||
badge.textContent = val;
|
||
badge.style.cssText = 'display:inline-block;background:#FAEEDA;color:#854F0B;' +
|
||
'padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600;' +
|
||
'letter-spacing:0.5px;vertical-align:middle;';
|
||
}
|
||
|
||
function checkDuplicateTableFieldValue(table, field, value, callback) {
|
||
if (!value || value.trim() === '') { callback(false); return; }
|
||
fetch(`<?= base_url('sales/checkDuplicate') ?>?table=${encodeURIComponent(table)}&field=${encodeURIComponent(field)}&value=${encodeURIComponent(value)}`)
|
||
.then(res => res.json())
|
||
.then(json => callback(json.exists === true))
|
||
.catch(() => callback(false));
|
||
}
|
||
|
||
function checkShortNameInBothTables(value, callback) {
|
||
let results = { clients: false, leads: false };
|
||
let completed = 0;
|
||
const done = () => { completed++; if (completed === 2) callback(results.clients || results.leads); };
|
||
checkDuplicateTableFieldValue('clients', 'short_name', value, (d) => { results.clients = d; done(); });
|
||
}
|
||
|
||
// ── Close dropdowns on outside click ─────────────────────────────
|
||
document.addEventListener('click', (e) => {
|
||
['add', 'edit'].forEach(mode => {
|
||
const ctx = COMPANY_CTX[mode];
|
||
if (!e.target.closest('#' + ctx.searchInput) && !e.target.closest('#' + ctx.dropdown)) {
|
||
const dd = document.getElementById(ctx.dropdown);
|
||
if (dd) dd.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
|
||
</script>
|