nhance/app/Views/sales/tracker_view.php
2026-02-26 15:18:19 +05:30

1821 lines
85 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<style>
/* POC Exact Styling */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* .top-bar { background: white; padding: 15px 30px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } */
.btn-primary { background: #ff6b35; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary:hover { background: #ff5722; transform: translateY(-1px); }
/* 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: #ff6b35; color: white; border-color: #ff6b35; }
/* 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; }
/* 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;}
.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; }
/* 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;
}
</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 class="my-0">
<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)">
<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-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('addLeadModal')">&times;</button>
</div>
<form class="parsley-examples" id="addLeadForm" enctype="multipart/form-data">
<hr class="my-0">
<div class="modal-body p-4">
<div class="form-group">
<div class="col-12 mb-1">
<div class="col-xl-12 col-lg-12 col-md-12">
<label class="form-label">Company Name<span class="text-danger">*</span></label>
<input type="text" class="form-control" name="company_name" style="width: 100%;" placeholder="Enter Company Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')" required>
</div>
</div>
<div class="form-row mb-1">
<div class="col-md-12 ml-3" style="width: 97%;">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\-_+]/g, '')">
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Phone</label>
<!-- <input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')"> -->
<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($users as $user): ?>
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="form-group text-right mb-0">
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('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>
<h2 id="det_company">Lead Detail</h2>
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
</div>
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('leadDetailModal')">&times;</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-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('activityModal')">&times;</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" onclick="selectType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Todo" onclick="selectType('Todo', this)">✓ To Do</button>
</div>
<div class="form-group">
<label class="form-label">Notes <span class="text-danger">*</span></label>
<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($users as $user): ?>
<option value="<?= $user['id'] ?>"><?= $user['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('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-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('completeModal')">&times;</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', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Todo', 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($users as $user): ?>
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="form-group text-right mb-0">
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('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">
<h2>Select Opportunity Type</h2>
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('opportunityModal')">&times;</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="opportunityModal">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>Add Opportunity</h2>
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('opportunityModal')">&times;</button>
</div>
<form class="parsley-examples" id="OpportunityForm" enctype="multipart/form-data">
<hr class="my-0">
<div class="modal-body p-4">
<input type="hidden" id="opp_lead_id">
<div class="form-group">
<label class="form-label">Opportunity Title</label>
<input type="text" name="title" class="search-input" style="width:100%">
</div>
<div class="form-row">
<div class="form-group ml-1">
<label class="form-label">Expected Worth Amount</label>
<input type="number" name="amount" class="search-input" style="width:100%">
</div>
<div class="form-group mr-1">
<label class="form-label">Policy Type</label>
<input type="text" name="policy_type" class="search-input" style="width:100%">
</div>
</div>
<div class="form-group">
<label class="form-label">Employee Count</label>
<input type="number" name="count" class="search-input" style="width:100%">
</div>
<div class="form-group">
<label class="form-label">Expected Close Date</label>
<input type="datetime-local" id="e_date" class="search-input" style="width:100%">
</div>
<div class="form-group">
<label class="form-label">Notes</label>
<textarea id="opp_notes" class="search-input" style="width:100%; height:100px;" placeholder="Add notes about this opportunity..." required></textarea>
</div>
</div>
<div class="modal-footer">
<div class="form-group text-right mb-0">
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('opportunityModal')">Cancel</button>
<button type="submit" class="btn-primary" >Create Opportunity</button>
</div>
</div>
</form>
</div>
</div> -->
<div class="modal" id="editLeadModal">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Edit Lead</h4>
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('editLeadModal')">&times;</button>
</div>
<form class="parsley-examples" id="editLeadForm" enctype="multipart/form-data">
<hr class="my-0">
<input type="hidden" class="form-control" id="hidden_lead_id" name="lead_id">
<div class="modal-body p-4">
<div class="form-group">
<div class="col-12 mb-1">
<div class="col-xl-12 col-lg-12 col-md-12">
<label class="form-label">Company Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="company_name" style="width: 100%;" placeholder="Enter Company Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')" required>
</div>
</div>
<div class="form-row mb-1">
<div class="col-md-12 ml-3" style="width: 97%;">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\-_+]/g, '')">
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Phone</label>
<!-- <input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')"> -->
<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>
<div class="col-12 mb-1" style="padding-left: 25px;">
<label class="form-label">Contact Persons</label>
<div id="contactPersonsContainer">
<div id="savedContactsContainer"> <span id="NoData"> <center><i> No contact persons added yet </i> </center> </span> </div>
</div>
</div>
<div class="col-12 mb-1 ml-1 ">
<div class="row g-2 align-items-center">
<div class="col-md-5" style="padding-left: 20px;">
<input type="text"
class="form-control"
id="contact_name"
placeholder="Contact Person Name"
oninput="this.value=this.value.replace(/[^A-Za-z\s]/g,'')">
</div>
<div class="col-md-4">
<input type="text"
class="form-control"
id="contact_mobile"
placeholder="Contact Mobile Number"
maxlength="10"
oninput="this.value=this.value.replace(/[^0-9]/g,'')">
</div>
<div class="col-md-3" style="padding-right: 4%;">
<button type="button"
id="btnSaveContact"
class="btn btn-sm w-100"
style="background:#ff6a3d;border:none;color:white;">
+ Save Contact
</button>
</div>
</div>
</div>
<div class="form-row mb-1">
<div class="col-md-12 ml-3" style="width: 97%;">
<label class="form-label">Status <span class="text-danger">*</span></label>
<select name="status" class="form-control" style="width: 100%;" required>
<option value="New">New</option>
<option value="Potential">Potential</option>
<option value="Prospects">Prospects</option>
<option value="Not a Prospects">Not a Prospects</option>
</select>
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Assign To <span class="text-danger">*</span> </label>
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
<option value="">Select User</option>
<?php foreach($users as $user): ?>
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="form-group text-right mb-0">
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('editLeadModal')">Cancel</button>
<button type="submit" class="btn-primary">Update Lead</button>
</div>
</div>
</form>
</div>
</div>
<script>
$(document).ready(function() {
resetFlatpicker();
$('.searchable').each(function() {
let parentModal = $(this).closest('.modal');
$(this).select2({
placeholder: "Select User",
dropdownParent: parentModal.length ? parentModal : $(document.body)
});
});
});
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 salesManagerIds = <?= json_encode($sales_manager_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).find('.searchable').val('').trigger('change'); // Clear jQuery/Select2/Searchable dropdowns if you use them
$(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';
}
}
}
function selectType(val, el) {
document.querySelectorAll('.activity-type-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
selectedType = val;
}
function selectFollowUpActivityType(val, el) {
document.querySelectorAll('.f_activity_type').forEach(b => b.classList.remove('active'));
el.classList.add('active');
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 salesManagerIds === 'undefined' || salesManagerIds.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=${salesManagerIds.join(',')}`; // We already proved it exists above!
if (typeof salesManagerIds !== 'undefined' && salesManagerIds.length > 0) {
url += `&assigned_to=${salesManagerIds.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 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" 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 cont = document.getElementById('opportunitiesContainer');
cont.innerHTML = opps.length ? opps.map(o => {
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
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">
<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> ${o.status}
</div>
</div>
<div class="opportunity-footer">
${o.notes || 'N/A'}
</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: "📄", Todo: "✓" };
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; margin-bottom:5px;">
<b>${icon} ${formattedType}</b>
<span style="font-size:11px; color:#999"> 👤 ${a.assigned_to_name} | 🗓 ${formattedDate}</span>
</div>
<div style="display:flex; justify-content:space-between; font-size:13px; color:#444;">
${a.notes}
</div>
<div style="font-size:13px; color:#444;"></div>
${a.status === 'pending' ?
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:8px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
`<div style="font-size:12px; color:#388e3c; margin-top:8px; 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').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').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}$/;
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
function showError(message, fieldName) {
toastr.warning(message, 'Validation Error');
setTimeout(function() {
$('[name="' + fieldName + '"]').focus();
}, 100);
}
// --- VALIDATION CHECKS (Button is still normal here) ---
if (!companyName && !email && !phone && !data.assigned_to) {
toastr.warning('Please fill in all the required fields.');
return;
}
if (!companyName) {
return showError('Company Name is required.', 'company_name');
}
if (!companyRegex.test(companyName)) {
return showError('Company Name can contain only letters and spaces.', 'company_name');
}
if (companyName.length < 2) {
return showError('Company Name must be at least 2 characters.', 'company_name');
}
if (!email && !phone) {
return showError("Either Email or Phone number is required.", 'email');
}
if (email && !emailRegex.test(email)) {
return showError("Please enter a valid email address.", 'email');
}
if (phone && !phoneRegex.test(phone)) {
// return showError("Phone number can contain only +, numbers and spaces (1020 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;
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 (!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 (1020 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');
closeModal('editLeadModal');
fetchLeads();
e.target.reset();
$('#savedContactsContainer').empty();
$('#NoData').show();
} else {
let err = await res.json();
if (res.status === 400) {
let errorMessages = "";
let seenMessages = [];
let isFirstError = true;
$('.form-control').removeClass('is-invalid');
if (err.messages) {
Object.entries(err.messages).forEach(([field, message]) => {
let inputElement = $('[name="' + field + '"]');
if (inputElement.length > 0) {
inputElement.addClass('is-invalid');
if (isFirstError) {
setTimeout(function() { inputElement.focus(); }, 100);
isFirstError = false;
}
}
if (!seenMessages.includes(message)) {
errorMessages += `• ${message}<br>`;
seenMessages.push(message);
}
});
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
} else {
toastr.warning(err.message || 'Validation failed', 'Warning');
}
} else {
toastr.error(err.message || 'Error updating lead');
}
}
} catch (err) {
toastr.error("A network error occurred.");
} finally {
// ALWAYS runs to reset button
submitBtn.disabled = false;
submitBtn.innerText = originalBtnText;
}
};
// Activity Form Submissions
document.getElementById('activityForm').onsubmit = async (e) => {
e.preventDefault();
// 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'
};
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'
};
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
}
});
const btnSaveContact = document.getElementById('btnSaveContact');
if (btnSaveContact) {
btnSaveContact.onclick = async (e) => {
// Use .value instead of .val()
let name = document.getElementById('contact_name').value.trim();
let mobile = document.getElementById('contact_mobile').value.trim();
let lead_id = document.getElementById('lead_id').value.trim();
if (!name || !mobile) {
return toastr.warning('Please enter both contact person name and mobile number.');
}
let payload = { lead_id: lead_id, name: name, mobile: mobile };
try {
const res = await fetch(`${API}/contacts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json(); // Get the response body
if (res.ok) {
// Clear inputs
document.getElementById('contact_name').value = '';
document.getElementById('contact_mobile').value = '';
// Hide "No Data" message
const noData = document.getElementById('NoData');
if(noData) noData.style.display = 'none';
// Create the HTML string
let contactHtml = `
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-2"
style="background: #f8f8f8; border-radius: 8px;"
data-id="${result.data.contact_id}">
<div>
<div class="fw-bold">${result.data.name}</div>
<div class="text-muted">${result.data.mobile}</div>
</div>
<button type="button"
class="btn btn-danger btn-sm btnRemoveContact"
data-id="${result.data.contact_id}">
Remove
</button>
</div>
`;
// <button type="button"
// class="btn btn-secondary btn-sm btnEditContact"
// data-id="${result.data.contact_id}">
// Update
// </button>
// Append to container using Vanilla JS
document.getElementById('savedContactsContainer').insertAdjacentHTML('beforeend', contactHtml);
toastr.success('Contact saved successfully');
} else {
let err = await res.json();
if (res.status === 400) {
let errorMessages = "";
let seenMessages = [];
if (err.messages) {
Object.entries(err.messages).forEach(([field, message]) => {
let inputElement = $('[name="' + field + '"]');
if (inputElement.length > 0) {
// Make the field box turn red so the user sees it immediately
inputElement.addClass('is-invalid');
// Focus on the first error field
if (isFirstError) {
// The 100ms delay safely bypasses Bootstrap's modal focus block
setTimeout(function() {
inputElement.focus();
}, 100);
isFirstError = false;
}
}
if (!seenMessages.includes(message)) {
errorMessages += `• ${message}<br>`;
seenMessages.push(message);
}
});
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
} else {
toastr.warning(err.message || 'Validation failed', 'Warning');
}
}
else {
toastr.error(err.message || 'Error adding lead');
}
}
} catch (error) {
console.error(error);
toastr.error('An error occurred');
}
};
}
const savedContactsContainer = document.getElementById('savedContactsContainer');
if (savedContactsContainer) {
savedContactsContainer.onclick = async (e) => {
// Check if the clicked element is a Remove button
if (e.target.classList.contains('btnRemoveContact')) {
const btn = e.target;
const contactId = btn.dataset.id; // Get data-id
const card = btn.closest('.contact-card'); // Find the parent card
try {
const res = await fetch(`${API}/contacts/${contactId}`, {
method: 'DELETE',
});
if (res.ok) {
card.remove(); // Remove from DOM
toastr.success('Contact removed successfully');
// Check if container is empty to show "No Data"
const container = document.getElementById('savedContactsContainer');
if (container.querySelectorAll('.contact-card').length === 0) {
const noData = document.getElementById('NoData');
if(noData) noData.style.display = 'block';
}
} else {
toastr.error('Failed to remove contact');
}
} catch (error) {
console.error(error);
toastr.error('An error occurred');
}
}
};
}
async function openEditLeadModal(id) {
// 1. Reset UI State
document.getElementById('hidden_lead_id').value = id;
const container = $('#savedContactsContainer');
// Remove only previous contact cards, keep the NoData span for now
container.find('.contact-card').remove();
try {
// 2. Fetch Lead Details
const response = await fetch(`${API}/leads/${id}`);
if (!response.ok) throw new Error('Failed to fetch lead');
const result = await response.json();
// IMPORTANT: Your data is nested inside result.data
const lead = result.data;
// 3. Populate Form Fields
const form = document.getElementById('editLeadForm');
// Mapping fields carefully
form.querySelector('[name="company_name"]').value = lead.company_name || '';
form.querySelector('[name="email"]').value = lead.email || '';
form.querySelector('[name="phone"]').value = lead.phone || '';
form.querySelector('[name="address"]').value = lead.address || '';
form.querySelector('[name="website"]').value = lead.website || '';
form.querySelector('[name="gst_number"]').value = lead.gst_number || '';
form.querySelector('[name="status"]').value = lead.status || 'New';
form.querySelector('[name="assigned_to"]').value = lead.assigned_to || '';
$(form.querySelector('[name="assigned_to"]')).trigger('change');
// 4. Handle Contact Persons (Looping through the nested array)
const contacts = lead.contact_persons; // Array from your JSON
if (contacts && contacts.length > 0) {
$('#NoData').hide();
contacts.forEach(contact => {
let contactHtml = `
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-3"
style="background: #f8f8f8; border-radius: 8px;"
data-id="${contact.contact_id}">
<div>
<div class="fw-bold">${contact.name}</div>
<div class="text-muted">${contact.mobile}</div>
</div>
<button type="button"
class="btn btn-danger btn-sm btnRemoveContact"
data-id="${contact.contact_id}">
Remove
</button>
</div>`;
container.append(contactHtml);
});
} else {
$('#NoData').show();
}
// 5. Open the Modal
openModal('editLeadModal');
} catch (error) {
console.error("Error loading lead data:", error);
alert("Could not load lead details. Check console for details.");
}
}
function convertDBFormatted(input) {
if (!input) return null;
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();
</script>