1821 lines
85 KiB
PHP
1821 lines
85 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: #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')">×</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')">×</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')">×</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')">×</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')">×</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')">×</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')">×</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 (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;
|
||
|
||
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 (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');
|
||
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>
|