1463 lines
68 KiB
PHP
1463 lines
68 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; }
|
||
|
||
.btn-primary { background: #02a8b5; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
|
||
.btn-primary:hover { background: #02a8b5; transform: translateY(-1px); }
|
||
.btn-complete { background: #4caf50; color: white; border: none;padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
|
||
.btn-complete:hover { background: #4caf50; transform: translateY(-1px); }
|
||
.btn-view { background: #f0f0f0; color: #666; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
|
||
.btn-view:hover { background: #f0f0f0; transform: translateY(-1px); }
|
||
.btn-close { background: none; border: none; font-size: 22px; cursor: pointer; color: #888; width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: background .2s; }
|
||
.btn-close:hover { background: #f0f0f0; color: #333; }
|
||
|
||
/* Filter Tabs */
|
||
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
||
.tab { padding: 10px 14px; border-radius: 20px; cursor: pointer; font-size: 12px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; white-space: nowrap; flex: 0 0 auto; }
|
||
.tab.active { background: #02a8b5; color: white; border-color: #02a8b5; }
|
||
|
||
/* Leads Grid */
|
||
.lead-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; /* responsive */ gap: 15px; }
|
||
.lead-actions { display: flex; align-items: center; gap: 10px; }
|
||
.sales-toolbar { flex-wrap: nowrap; padding: 16px 15px; gap: 12px; overflow-x: auto; }
|
||
.sales-toolbar .filter-tabs { padding: 0; flex: 0 0 auto; min-width: max-content; overflow: visible; }
|
||
.sales-toolbar .lead-actions { flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; justify-content: flex-end; }
|
||
.sales-toolbar .search-input { width: 230px !important; height: 38px; padding: 9px 12px; border: 1px solid #ddd !important; border-radius: 6px !important; flex: 0 0 230px; }
|
||
.sales-toolbar .toolbar-select { width: 175px !important; min-width: 175px; padding: 9px 12px; }
|
||
.sales-toolbar .toolbar-select + .select2-container { width: 175px !important; min-width: 175px; }
|
||
.sales-toolbar .financial-year-select { width: 122px !important; min-width: 122px; }
|
||
.sales-toolbar .financial-year-select + .select2-container { width: 122px !important; min-width: 122px; flex: 0 0 122px; }
|
||
.sales-toolbar .select2-container .select2-selection--single { height: 38px; border: 1px solid #ddd; border-radius: 6px; display: flex; align-items: center; }
|
||
.sales-toolbar .select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 36px; }
|
||
.sales-toolbar .select2-container--default .select2-selection--single .select2-selection__arrow { height: 36px; }
|
||
.sales-toolbar #memberFilter + .select2-container { width: 175px !important; min-width: 175px; flex: 0 0 175px; }
|
||
.sales-toolbar #memberFilter + .select2-container .select2-selection--single,
|
||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection--single { height: 38px !important; background: #fff !important; border: 1px solid #ddd !important; border-radius: 6px !important; display: flex; align-items: center; }
|
||
.sales-toolbar #memberFilter + .select2-container .select2-selection__rendered,
|
||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection__rendered { flex: 1; line-height: 36px !important; padding-left: 12px; padding-right: 28px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.sales-toolbar #memberFilter + .select2-container .select2-selection__arrow,
|
||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow { height: 25px !important; width: 20px; top: 1px; right: 6px; }
|
||
.sales-toolbar #memberFilter + .select2-container .select2-selection__arrow b,
|
||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow b { border-width: 4px 3px 0 3px !important; margin-left: -3px; margin-top: -2px; }
|
||
.sales-toolbar #memberFilter + .select2-container--open .select2-selection__arrow b,
|
||
.sales-toolbar #financialYearFilter + .select2-container--open .select2-selection__arrow b { border-width: 0 3px 4px 3px !important; }
|
||
.toolbar-icon-btn { width: 38px; height: 38px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 8px; color: #fff; cursor: pointer; transition: all 0.2s; }
|
||
.toolbar-icon-btn:hover { transform: translateY(-1px); filter: brightness(0.96); }
|
||
.toolbar-icon-btn:focus { outline: none; box-shadow: 0 0 0 2px rgba(2, 168, 181, 0.25); }
|
||
.toolbar-icon-btn i { font-size: 20px; line-height: 1; }
|
||
.toolbar-icon-btn.export-btn { background: #1f9d55; }
|
||
.toolbar-icon-btn.add-btn { background: #02a8b5; }
|
||
/* This forces 2 equal columns on desktop */
|
||
.activities-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; padding: 30px; }
|
||
|
||
/* .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); } */
|
||
.activity-card { background: white; border-radius: 12px; padding: 20px; border: 1px solid #e0e0e0; transition: all 0.2s;}
|
||
.activity-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); }
|
||
.activity-card-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 15px;}
|
||
.activity-type-badge { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; background: #f0f0f0; border-radius: 20px; font-size: 13px; font-weight: 500;}
|
||
.activity-status-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500;}
|
||
.activity-details { margin-bottom: 15px;}
|
||
.activity-actions { display: flex; gap: 10px; padding-top: 15px; border-top: 1px solid #f0f0f0;}
|
||
|
||
|
||
/* .activity-notes { margin: 10px 0; font-size: 14px; color: #666; line-height: 1.5;
|
||
* Limit to exactly 2 lines *
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
|
||
* Make it look like it's interactive *
|
||
cursor: help;
|
||
min-height: 3em; /* Keeps card heights consistent even if text is 1 line *
|
||
} */
|
||
|
||
.activity-notes {
|
||
display: block;
|
||
margin-bottom: 15px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
/* Standard 2-row clamp */
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2; /* The legacy WebKit version */
|
||
line-clamp: 2; /* The official standard version */
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
height: 3em;
|
||
cursor: help;
|
||
}
|
||
|
||
/* Optional: Slight highlight on hover */
|
||
.activity-notes:hover { color: #000; }
|
||
.activity-meta { display: flex; gap: 20px; font-size: 13px; color: #999; margin-top: 10px;}
|
||
|
||
.lead-status { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; margin-top: 5px; }
|
||
.opportunity-status-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500;}
|
||
|
||
.status-new { background: #e3f2fd; color: #1976d2; }
|
||
.status-potential { background: #fff3e0; color: #f57c00; }
|
||
.status-prospects { background: #e8f5e9; color: #388e3c; }
|
||
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
||
.status-pending { background: #fff3e0; color: #f57c00; }
|
||
.status-completed { background: #e8f5e9; color: #388e3c; }
|
||
.status-unknown { background: #000; color: #fff; }
|
||
.status-text-unknown { color: #000; font-weight: bold; }
|
||
.status-text-lost { color: #d32f2f; font-weight: bold; }
|
||
.status-text-won { color: #388e3c; font-weight: bold; }
|
||
|
||
|
||
/* Modals */
|
||
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center; }
|
||
.modal.active { display: flex; }
|
||
.modal-content { background: white; border-radius: 12px; width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; }
|
||
.modal-header { padding: 5px 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; }
|
||
.modal-body { padding: 25px; flex: 1; }
|
||
.form-group { margin-bottom: 20px; }
|
||
.form-label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; }
|
||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
|
||
|
||
/* Activity Type Buttons (POC Style) */
|
||
.activity-types { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px; }
|
||
.activity-type-btn { padding: 12px; border: 1px solid #e0e0e0; background: white; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 14px; transition: 0.2s; }
|
||
.activity-type-btn:hover { border-color: #ff6b35; background: #fff5f2; }
|
||
.activity-type-btn.active { border-color: #ff6b35; background: #ff6b35; color: white; }
|
||
|
||
.activity-types { display: grid; grid-template-columns: repeat(12, 1fr); /* 12-column grid */ gap: 10px; margin-bottom: 20px; }
|
||
|
||
/* First 3 buttons → 4 columns each (3 × 4 = 12) */
|
||
.activity-types button:nth-child(-n+3) { grid-column: span 4; }
|
||
|
||
/* Next 4 buttons → 3 columns each (4 × 3 = 12) */
|
||
.activity-types button:nth-child(n+4) { grid-column: span 3; }
|
||
|
||
/* Timeline Styling */
|
||
.timeline { position: relative; padding-left: 30px; margin-top: 20px; }
|
||
.timeline-item { position: relative; padding-bottom: 25px; }
|
||
.timeline-item::before { content: ''; position: absolute; left: -21px; top: 10px; width: 2px; height: 100%; background: #e0e0e0; }
|
||
.timeline-dot { position: absolute; left: -26px; top: 2px; width: 12px; height: 12px; border-radius: 50%; background: #ff6b35; border: 2px solid white; box-shadow: 0 0 0 1px #ff6b35; }
|
||
.timeline-dot.completed { background: #4caf50; box-shadow: 0 0 0 1px #4caf50; }
|
||
.timeline-content { background: #f8f8f8; padding: 15px; border-radius: 8px; }
|
||
.timeline-item {display : block !important;}
|
||
.timeline::before { display: none !important; content: none !important; }
|
||
.timeline.no-line::before { display: none; }
|
||
.timeline { border-left: none !important; }
|
||
.timeline-item:last-child { border-left: none !important;}
|
||
.timeline-item:last-child::before { display: none !important;}
|
||
|
||
/* opportunities Styling */
|
||
.opportunities-list { display: grid; gap: 15px; margin-bottom: 12px; }
|
||
.opportunity-card { background: white; border-radius: 12px; padding: 20px; border: 1px solid #e0e0e0; transition: all 0.2s;}
|
||
.opportunity-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 15px;}
|
||
.opportunity-title { font-weight: 600; font-size: 16px; margin-bottom: 5px;}
|
||
.opportunity-amount { font-size: 20px; font-weight: 700; color: #4caf50;}
|
||
.opportunity-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;}
|
||
.opportunity-detail-item { font-size: 13px; color: #666;}
|
||
.opportunity-footer { margin-top: 15px; padding-top: 15px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #666; }
|
||
.lost-reason { margin-bottom: 8px; color: #c0392b; /* soft red for lost */ }
|
||
.footer-divider { border-top: 1px dashed #ddd; margin: 8px 0; }
|
||
.notes { color: #555; }
|
||
|
||
/* Base style for both tabs */
|
||
.tab-item { cursor: pointer; padding-bottom: 10px; margin: 0; font-size: 16px; color: #999; /* Default grey for unselected */ border-bottom: 2px solid transparent; transition: all 0.2s ease; }
|
||
|
||
/* Style for the SELECTED tab */
|
||
.tab-item.active { color: #333; /* Darker text */ border-bottom: 2px solid #ff6b35; /* Orange underline */ }
|
||
|
||
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
|
||
.empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px; }
|
||
input, select, textarea { background-color: white !important; border-radius: 6px !important; box-shadow: none !important; border-color: #ddd !important; }
|
||
input, select, textarea { width: 400px; padding: 10px 15px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; outline: none; }
|
||
input:focus, select:focus, textarea:focus { border-color: #ff6b35; outline: none; /* removes blue browser outline */ }
|
||
.modal .form-control { transition: 0.2s ease; }
|
||
|
||
.modal .form-control:focus { border-color: #ff6b35; box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.2);
|
||
outline: none;
|
||
}
|
||
|
||
.select2-container--default
|
||
.select2-selection--multiple
|
||
.select2-selection__choice {
|
||
background-color: #02a8b5 !important;
|
||
border: none !important;
|
||
border-color: #fff !important;
|
||
color: #fff !important;
|
||
}
|
||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||
color: #fff !important;
|
||
}
|
||
.modal .select2-container--default .select2-selection--multiple {
|
||
background-color: #fff !important;
|
||
}
|
||
|
||
|
||
</style>
|
||
|
||
<div class="main-content">
|
||
<hr style="margin-bottom: 0 !important;">
|
||
<div class="lead-header sales-toolbar">
|
||
|
||
<!-- LEFT SIDE -->
|
||
<div class="filter-tabs">
|
||
<div class="tab active" data-filter="all" onclick="setFilter('all', this)">All</div>
|
||
<div class="tab" data-filter="Pending" onclick="setFilter('pending', this)">Pending</div>
|
||
<div class="tab" data-filter="Completed" onclick="setFilter('completed', this)">Completed</div>
|
||
</div>
|
||
|
||
<!-- RIGHT SIDE -->
|
||
<div class="lead-actions">
|
||
<input type="text" class="search-input" id="mainSearch"
|
||
placeholder="Search activities..." onkeyup="fetchActivities(false)">
|
||
<select id="memberFilter" class="toolbar-select searchable" onchange="fetchActivities(false)">
|
||
<option value="" selected>All Member</option>
|
||
<?php foreach(($sales_manager_with_head ?? []) as $sm): ?>
|
||
<option value="<?= $sm['id'] ?>"><?= esc($sm['first_name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<select id="financialYearFilter" class="toolbar-select financial-year-select searchable" onchange="fetchActivities(false)">
|
||
<?php foreach(($fin_years ?? []) as $year): ?>
|
||
<option value="<?= esc($year) ?>" <?= (($current_fin_year ?? '') === $year) ? 'selected' : '' ?>><?= esc($year) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button class="toolbar-icon-btn export-btn" onclick="exportActivitiesCsv()" title="Export" aria-label="Export activities">
|
||
<i class="mdi mdi-file-excel"></i>
|
||
</button>
|
||
<button class="toolbar-icon-btn add-btn" onclick="openMainActivityModal()" title="Add Activity" aria-label="Add activity">
|
||
<i class="mdi mdi-plus"></i>
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="activities-grid" id="activitiesGrid"></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="fetchActivities(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="leadDetailModal">
|
||
<div class="modal-content" style="max-width: 850px;">
|
||
<div class="modal-header">
|
||
<div>
|
||
<h4 class="modal-title" id="det_company" style="margin-top: 5px;" >Lead Detail</h2>
|
||
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
||
</div>
|
||
<button class="btn-close" 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;" onclick="openOpportunityModal()" id="btn_add_opportunity">+ Add Opportunity</button>
|
||
</div>
|
||
<div id="opportunitiesContainer" class="Opportunities"></div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="activityModal">
|
||
<div class="modal-content" style="max-width: 600px;">
|
||
<div class="modal-header">
|
||
<h2>Add Activity</h2>
|
||
<button class="btn-close" 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_string_flag"> <!-- ADD THIS -->
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Select Lead <span class="text-danger">*</span></label>
|
||
<select id="act_lead_id" class="search-input searchable" style="width:100%" required>
|
||
<?php foreach($leads as $lead): ?>
|
||
<option value="<?= $lead['lead_id'] ?>"><?= $lead['company_name'] ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Activity Type <span class="text-danger">*</span></label>
|
||
<div class="activity-types" id="typeButtons">
|
||
<button type="button" class="activity-type-btn d_activity_type active" data-type="Call" onclick="selectType('Call', this)">📞 Call</button>
|
||
<button type="button" class="activity-type-btn d_activity_type" data-type="Email" onclick="selectType('Email', this)">✉️ Email</button>
|
||
<button type="button" class="activity-type-btn d_activity_type" data-type="Meeting" onclick="selectType('Meeting', this)">📅 Meeting</button>
|
||
<button type="button" class="activity-type-btn d_activity_type" data-type="Visit" onclick="selectType('Visit', this)">🚗 Visit</button>
|
||
<button type="button" class="activity-type-btn d_activity_type" data-type="Demo" onclick="selectType('Demo', this)">🖥️ Demo</button>
|
||
<button type="button" class="activity-type-btn d_activity_type" data-type="Share Docs" onclick="selectType('Share Docs', this)">📄 Share Docs</button>
|
||
<button type="button" class="activity-type-btn d_activity_type" data-type="To Do" onclick="selectType('To Do', this)">✓ To Do</button>
|
||
</div>
|
||
</div>
|
||
<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 <span class="text-danger">*</span></label>
|
||
<select id="act_owner" class="search-input searchable" style="width:100%" required>
|
||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Additional Assigner</label>
|
||
<select id="act_multiple_owner" class="search-input searchable multi-searchable" style="width:100%;" multiple>
|
||
<?php foreach($sales_team as $sm): ?>
|
||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="form-group text-right mb-0">
|
||
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('activityModal')">Cancel</button>
|
||
<button type="submit" class="btn-primary">Create Activity</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="completeModal">
|
||
<div class="modal-content" style="max-width: 675px;">
|
||
<div class="modal-header">
|
||
<h3>Complete Activity</h3>
|
||
<button class="btn-close" 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">
|
||
<input type="hidden" id="string_flag">
|
||
<div class="form-group">
|
||
<label class="form-label">Outcome / Notes <span class="text-danger">*</span></label>
|
||
<textarea id="comp_notes" class="search-input" style="width:100%; height:80px;" required placeholder="What happened? Any next steps?" rows="3" required></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Next Follow-up? <span class="text-danger">*</span></label>
|
||
<select id="do_follow" class="search-input" onchange="document.getElementById('f_up').style.display=this.value==='yes'?'block':'none'" style="width:100%;" required>
|
||
<option value="no">No</option>
|
||
<option value="yes">Yes</option>
|
||
</select>
|
||
</div>
|
||
<div id="f_up" style="display:none; border-top:1px dashed #ccc; padding-top:15px;">
|
||
<label class="form-label">Next Activity Type <span class="text-danger">*</span></label>
|
||
<div class="activity-types" id="f_typeButtons">
|
||
|
||
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Call', this)">📞 Call</button>
|
||
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Email', this)">✉️ Email</button>
|
||
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Meeting', this)">📅 Meeting</button>
|
||
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Visit', this)">🚗 Visit</button>
|
||
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Demo', this)">🖥️ Demo</button>
|
||
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Share Docs', this)">📄 Share Docs</button>
|
||
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('To Do', this)">✓ To Do</button>
|
||
|
||
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Follow-up Notes <span class="text-danger">*</span></label>
|
||
<textarea id="f_notes" class="search-input" style="width:100%; height:80px;"></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">Scheduled Date & Time <span class="text-danger">*</span></label>
|
||
<div class="input-icon">
|
||
<input type="text" class="search-input forschedule" id="f_date" style="width:100%;" placeholder="DD/MM/YYYY hh:mm:ss AM(or)PM">
|
||
<i class="mdi mdi-calendar-blank-outline additional-icon"></i>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
|
||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Additional Assigner <span class="text-danger">*</span></label>
|
||
<select id="f_multiple_owner" class="search-input searchable multi-searchable" style="width:100%" multiple>
|
||
<?php foreach($sales_team as $sm): ?>
|
||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="form-group text-right mb-0">
|
||
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('completeModal')">Cancel</button>
|
||
<button type="submit" class="btn-primary">Submit Outcome</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="opportunityModal">
|
||
<div class="modal-content" style="max-width: 600px;">
|
||
<div class="modal-header">
|
||
<h4 class="modal-title" style="margin-top: 15px;margin-bottom: 15px;">Select Opportunity Type</h4>
|
||
<button class="btn-close" 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>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
resetFlatpicker();
|
||
|
||
// Single selects
|
||
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||
let parentModal = el.closest('.modal');
|
||
const select2Options = {
|
||
allowClear: false,
|
||
minimumResultsForSearch: 0,
|
||
dropdownParent: parentModal ? $(parentModal) : $(document.body)
|
||
};
|
||
if (el.id !== 'memberFilter') {
|
||
select2Options.placeholder = "Select..";
|
||
}
|
||
$(el).select2(select2Options);
|
||
});
|
||
|
||
// Multi selects
|
||
document.querySelectorAll('.multi-searchable').forEach(function(el) {
|
||
let parentModal = el.closest('.modal');
|
||
$(el).select2({
|
||
placeholder: "Search and select members...",
|
||
allowClear: false,
|
||
closeOnSelect: false,
|
||
minimumResultsForSearch: 0,
|
||
dropdownParent: parentModal ? $(parentModal) : $(document.body),
|
||
width: '100%'
|
||
});
|
||
});
|
||
});
|
||
|
||
// $(document).ready(function() {
|
||
// resetFlatpicker();
|
||
// $('.searchable').each(function() {
|
||
// let parentModal = $(this).closest('.modal');
|
||
// $(this).select2({
|
||
// placeholder: "Select..",
|
||
// 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 activityIcons = {
|
||
"Call": "📞",
|
||
"Email": "✉️",
|
||
"Meeting": "📅",
|
||
"Visit": "🚗",
|
||
"Demo": "🖥️",
|
||
"Share Docs": "📄",
|
||
"To Do": "✓"
|
||
};
|
||
const salesManagerWithHeadIds = <?= json_encode($sales_manager_with_head_ids ?? []) ?>;
|
||
|
||
const API = '<?= base_url('sales') ?>';
|
||
const defaultFinancialYear = '<?= esc($current_fin_year ?? '') ?>';
|
||
let filter = 'all';
|
||
let lead_id = null;
|
||
let global_lead_assigned_to = null;
|
||
let selectedType = 'Call';
|
||
let selectedFollowUpActivityType = '';
|
||
let currentPage = 1;
|
||
let limit = 10;
|
||
let currentOffset = 0;
|
||
const department = '<?= $sales_role ?>';
|
||
|
||
function getCurrentFY() {
|
||
const today = new Date();
|
||
const year = today.getFullYear();
|
||
const month = today.getMonth();
|
||
const startYear = month >= 3 ? year : year - 1;
|
||
return startYear + '-' + String(startYear + 1);
|
||
}
|
||
|
||
function initSalesToolbarFilters() {
|
||
const fySelect = document.getElementById('financialYearFilter');
|
||
const params = new URLSearchParams(window.location.search);
|
||
|
||
if (fySelect) {
|
||
let fy = params.get('fy') || defaultFinancialYear || getCurrentFY();
|
||
const matched = Array.from(fySelect.options).some(opt => opt.value === fy);
|
||
if (!matched && fySelect.options.length > 0) {
|
||
fy = fySelect.options[0].value;
|
||
}
|
||
fySelect.value = fy;
|
||
}
|
||
|
||
const statusParam = (params.get('status') || 'all').toLowerCase();
|
||
const statusTab = Array.from(document.querySelectorAll('.filter-tabs .tab'))
|
||
.find(tab => (tab.dataset.filter || '').toLowerCase() === statusParam);
|
||
if (statusTab) {
|
||
document.querySelectorAll('.filter-tabs .tab').forEach(tab => tab.classList.remove('active'));
|
||
statusTab.classList.add('active');
|
||
filter = statusParam;
|
||
}
|
||
|
||
const memberSelect = document.getElementById('memberFilter');
|
||
const memberId = params.get('member');
|
||
if (memberSelect && memberId && Array.from(memberSelect.options).some(opt => opt.value === memberId)) {
|
||
memberSelect.value = memberId;
|
||
if (window.jQuery) {
|
||
$(memberSelect).trigger('change.select2');
|
||
}
|
||
}
|
||
}
|
||
|
||
function getSelectedMemberIds(defaultIds) {
|
||
const memberFilter = document.getElementById('memberFilter');
|
||
if (memberFilter && memberFilter.value) {
|
||
return [memberFilter.value];
|
||
}
|
||
return Array.isArray(defaultIds) ? defaultIds : [];
|
||
}
|
||
|
||
function getSelectedFinancialYear() {
|
||
return document.getElementById('financialYearFilter')?.value || defaultFinancialYear || getCurrentFY();
|
||
}
|
||
|
||
|
||
function openModal(id) { document.getElementById(id).classList.add('active'); resetFlatpicker(); }
|
||
|
||
function closeModal(id) {
|
||
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');
|
||
fetchActivities(); // now i called because of Refresh the page .......
|
||
}
|
||
|
||
// 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 openMainActivityModal() {
|
||
document.getElementById('act_owner').value = "";
|
||
document.getElementById('act_owner').dispatchEvent(new Event('change'));
|
||
document.getElementById('act_lead_id').value = "";
|
||
document.getElementById('act_lead_id').dispatchEvent(new Event('change'));
|
||
document.getElementById('act_string_flag').value = "frommain"; // ✅ flag for main
|
||
openModal('activityModal');
|
||
}
|
||
|
||
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;
|
||
const searchInput = document.getElementById('mainSearch');
|
||
if (searchInput) {
|
||
searchInput.value = '';
|
||
}
|
||
fetchActivities(false);
|
||
}
|
||
|
||
async function fetchActivities(isLoadMore = false) {
|
||
const q = document.getElementById('mainSearch').value;
|
||
const grid = document.getElementById('activitiesGrid');
|
||
const btnLoadMore = document.getElementById('btn-load-more');
|
||
const spinner = document.getElementById('load-more-spinner');
|
||
const text = document.getElementById('load-more-text');
|
||
const selectedMemberIds = getSelectedMemberIds(salesManagerWithHeadIds);
|
||
|
||
|
||
// 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 Activities found</p>
|
||
</div>
|
||
</div>`;
|
||
|
||
console.log("function here");
|
||
console.log(salesManagerWithHeadIds);
|
||
// return;
|
||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
||
if (selectedMemberIds.length === 0) {
|
||
console.log("No Sales Manager IDs found. Skipping API call.");
|
||
grid.innerHTML = emptyStateHTML;
|
||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||
|
||
// Reset spinner just in case
|
||
if (isLoadMore) {
|
||
spinner.style.display = 'none';
|
||
text.innerText = 'Load more';
|
||
}
|
||
return; // This completely stops the function here. No API call is made.
|
||
}
|
||
|
||
// 3. If we are doing a fresh search or changing tabs, reset offset to 0
|
||
if (!isLoadMore) {
|
||
currentOffset = 0;
|
||
}
|
||
|
||
// 4. Show spinner on button
|
||
if (isLoadMore) {
|
||
spinner.style.display = 'inline-block';
|
||
text.innerText = 'Loading...';
|
||
}
|
||
|
||
// 5. Build URL with dynamic offset
|
||
const params = new URLSearchParams({
|
||
status: filter === 'all' ? '' : filter,
|
||
search: q,
|
||
limit,
|
||
offset: currentOffset,
|
||
assigned_to: selectedMemberIds.join(','),
|
||
financial_year: getSelectedFinancialYear()
|
||
});
|
||
let url = `${API}/activities?${params.toString()}`;
|
||
|
||
console.log("Fetching API:", url);
|
||
|
||
try {
|
||
const res = await fetch(url);
|
||
const json = await res.json();
|
||
|
||
const activityData = json.data || [];
|
||
const totalrecords = json.total || 0;
|
||
console.log("API Response:", activityData);
|
||
|
||
const tabLeadsCount = json.counts || {};
|
||
|
||
// ✅ Update tab counts
|
||
if (tabLeadsCount) {
|
||
document.querySelector('[data-filter="all"]').innerText = `All (${tabLeadsCount.all ?? 0})`;
|
||
document.querySelector('[data-filter="Pending"]').innerText = `Pending (${tabLeadsCount.pending ?? 0})`;
|
||
document.querySelector('[data-filter="Completed"]').innerText = `Completed (${tabLeadsCount.completed ?? 0})`;
|
||
}
|
||
|
||
const html = activityData
|
||
.sort((a, b) => Number(b.activity_id) - Number(a.activity_id))
|
||
.map(a => {
|
||
let formattedCreatedDate = a.created_at
|
||
? new Date(a.created_at).toLocaleString('en-US', {
|
||
month: 'short', day: 'numeric', year: 'numeric',
|
||
hour: '2-digit', minute: '2-digit', hour12: true
|
||
})
|
||
: 'N/A';
|
||
|
||
let displayNote = 'N/A';
|
||
if (a.status.toLowerCase() === 'pending') {
|
||
displayNote = a.notes || 'No pending notes';
|
||
} else if (a.status.toLowerCase() === 'completed') {
|
||
displayNote = a.completion_notes || 'No completion notes';
|
||
}
|
||
|
||
// 2. Clean the note for the tooltip (prevents quotes from breaking HTML)
|
||
const tooltipNote = displayNote.replace(/"/g, '"');
|
||
|
||
return `
|
||
<div class="activity-card">
|
||
<div class="activity-card-header">
|
||
<span class="activity-type-badge">
|
||
${activityIcons[a.activity_type]} ${a.activity_type}
|
||
</span>
|
||
<span class="activity-status-badge status-${a.status.toLowerCase().replace(' ', '-')}"">
|
||
${capitalize(a.status)}
|
||
</span>
|
||
</div>
|
||
<div class="activity-details">
|
||
<div class="activity-lead-name">${a.company_name}</div>
|
||
<div class="activity-notes" title="${displayNote}">${displayNote}</div>
|
||
<div class="activity-meta">
|
||
<span>🗓️ ${formattedCreatedDate}</span>
|
||
<span>👤 ${a.assigned_to_name}</span>
|
||
</div>
|
||
${a.additional_assigned_names ? `
|
||
<div style="display:flex; justify-content:flex-start; margin-bottom:5px;">
|
||
<span style="font-size:11px; color:#999; text-align: right;
|
||
max-width: 75%; /* Forces long lists to wrap cleanly on the right side */
|
||
display: inline-block;">
|
||
👥 ${a.additional_assigned_names}
|
||
</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
|
||
<div class="activity-actions">
|
||
<button style="display:${a.status == 'completed' ? 'none' : 'block'};" class="btn-complete" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id},'frommain')">
|
||
✓ Complete
|
||
</button>
|
||
<button class="btn-view" onclick="viewDetail(${a.lead_id})">
|
||
View Details
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
// <a class="dropdown-item text-danger" href="javascript:void(0);"> 🗑 Delete</a>
|
||
|
||
// 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 += activityData.length;
|
||
|
||
// 8. Stop spinner
|
||
spinner.style.display = 'none';
|
||
text.innerText = 'Load more';
|
||
|
||
// 9. Hide button if the API returns LESS than our limit
|
||
if (activityData.length < limit || activityData.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;
|
||
let res = await fetch(`${API}/leads/${id}`);
|
||
let json = await res.json();
|
||
let 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');
|
||
// Use a fallback string like 'unknown' or 'pending'
|
||
let status = l.status || 'unknown';
|
||
|
||
badge.innerText = status;
|
||
badge.className = `lead-status status-${status.toLowerCase().replace(' ', '-')}`;
|
||
|
||
const oppBtn = document.getElementById('btn_add_opportunity');
|
||
if (l.status === 'Prospects') {
|
||
oppBtn.style.display = 'inline-block';
|
||
} else {
|
||
oppBtn.style.display = 'none';
|
||
}
|
||
|
||
renderTimeline(l.activities);
|
||
renderCard(l.opportunities || []);
|
||
openModal('leadDetailModal');
|
||
}
|
||
|
||
function renderCard(opps) {
|
||
const opp_cont = document.getElementById('opportunitiesContainer');
|
||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
|
||
let status = o.status?.toLowerCase();
|
||
let statusClass = {
|
||
won: 'status-text-won',
|
||
lost: 'status-text-lost'
|
||
}[status] || 'status-text-unknown';
|
||
|
||
let opp_status_value = o.status?.replace(/[-_']/g, ' ').toUpperCase();
|
||
|
||
let formattedDate = new Date(o.created_at).toLocaleString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: true
|
||
});
|
||
// ${o.client_short_name}
|
||
return `
|
||
<div class="opportunities-list" id="leadOpportunitiesList">
|
||
<div class="opportunity-card" style="bottom:2px;">
|
||
<div class="opportunity-header">
|
||
<div class="opportunity-title">${lead_type}</div>
|
||
</div>
|
||
<div class="opportunity-details">
|
||
<div class="opportunity-detail-item">
|
||
<strong>Client Name :</strong> ${o.client_name}
|
||
</div>
|
||
<div class="opportunity-detail-item">
|
||
<strong>Client Code:</strong> ${o.client_code}
|
||
</div>
|
||
<div class="opportunity-detail-item">
|
||
<strong>Created At:</strong> ${formattedDate}
|
||
</div>
|
||
<div class="opportunity-detail-item">
|
||
<strong>Status:</strong> <span class="${statusClass}">${opp_status_value}</span>
|
||
</div>
|
||
</div>
|
||
<div class="opportunity-footer">
|
||
${ `${o.status === 'lost' ? `
|
||
<div class="lost-reason">
|
||
<strong>Reason for Loss:</strong> ${o.lost_reason || 'N/A'}
|
||
</div>
|
||
<div class="footer-divider"></div>
|
||
` : ``}
|
||
<div class="notes">
|
||
<strong>Remarks:</strong> ${o.notes || 'N/A'}
|
||
</div>
|
||
`
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('') : `<div class="empty-state"> <div class="empty-icon">💼</div> No opportunities yet. Add one to get started!</div>`;
|
||
}
|
||
|
||
function renderTimeline(acts) {
|
||
const cont = document.getElementById('timelineContainer');
|
||
|
||
|
||
if (acts.length === 0) {
|
||
cont.classList.add('no-line');
|
||
} else {
|
||
cont.classList.remove('no-line');
|
||
}
|
||
|
||
cont.innerHTML = acts.length ? acts.map(a => {
|
||
|
||
const icon = activityIcons[a.activity_type] || "";
|
||
const formattedType = a.activity_type?.toUpperCase() || "";
|
||
const formattedDate = new Date(a.scheduled_date).toLocaleString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: true
|
||
});
|
||
|
||
// Then use the variable in your HTML:
|
||
// <span style="font-size:11px; color:#999">${formattedDate}</span>
|
||
|
||
return `
|
||
<div class="timeline-item">
|
||
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
|
||
<div class="timeline-content">
|
||
|
||
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:2px;">
|
||
<b style="white-space: nowrap; margin-right: 15px;">${icon} ${formattedType}</b>
|
||
<span style="font-size:11px; color:#999; text-align:right;"> 👤 ${a.assigned_to_name} | 🗓️ ${formattedDate}</span>
|
||
</div>
|
||
|
||
${a.additional_assigned_names ? `
|
||
<div style="display:flex; justify-content:flex-end; margin-bottom:5px;">
|
||
<span style="font-size:11px; color:#999; text-align: right;
|
||
max-width: 75%; /* Forces long lists to wrap cleanly on the right side */
|
||
display: inline-block;">
|
||
👥 ${a.additional_assigned_names}
|
||
</span>
|
||
</div>
|
||
` : '<div style="margin-bottom:8px;"></div>'}
|
||
|
||
<div style="font-size:13px; color:#444; margin-bottom: 8px;">
|
||
${a.notes}
|
||
</div>
|
||
|
||
${a.status === 'pending' ?
|
||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:4px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
|
||
`<div style="font-size:12px; color:#388e3c; margin-top:4px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||
}
|
||
</div>
|
||
</div>`;
|
||
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</div>`;
|
||
}
|
||
|
||
function openActivityModal() {
|
||
|
||
document.getElementById('act_owner').value = global_lead_assigned_to;
|
||
document.getElementById('act_owner').dispatchEvent(new Event('change'));
|
||
document.getElementById('act_lead_id').value = lead_id;
|
||
document.getElementById('act_lead_id').dispatchEvent(new Event('change'));
|
||
document.getElementById('act_string_flag').value = "frompopup";
|
||
openModal('activityModal');
|
||
}
|
||
|
||
function openComp(id, assigned_to, lead_id, string_flag) {
|
||
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('string_flag').value = string_flag;
|
||
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 capitalize(str) {
|
||
if (!str) return '';
|
||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||
}
|
||
|
||
function switchTab(tabName) {
|
||
//Default Activity Tab how to resset here
|
||
// 1. Hide all tab content
|
||
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');
|
||
}
|
||
|
||
// 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();
|
||
let actLead = document.getElementById('act_lead_id').value.trim();
|
||
let flag = document.getElementById('act_string_flag').value;
|
||
|
||
|
||
// 2. Perform Validation
|
||
if (!actLead) {
|
||
toastr.warning('Please Select the Lead');
|
||
return;
|
||
}
|
||
|
||
if (!actNotes) {
|
||
toastr.warning('Notes is required');
|
||
return;
|
||
}
|
||
|
||
const hasDefaultActiveType = document.querySelector('#typeButtons .d_activity_type.active');
|
||
if (!hasDefaultActiveType) {
|
||
toastr.warning('Please select an activity type.');
|
||
return;
|
||
}
|
||
if (!selectedType) {
|
||
toastr.warning('Please select an activity type.');
|
||
return;
|
||
}
|
||
|
||
if (!feFormat.trim()) {
|
||
toastr.warning('Scheduled date & time is required');
|
||
return;
|
||
}
|
||
|
||
if (!actOwner) {
|
||
toastr.warning('Please Select the user to be Assigned');
|
||
return;
|
||
}
|
||
|
||
// 3. Validation passed! Now disable the button and change text
|
||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||
const originalBtnText = submitBtn.innerText;
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerText = 'Creating...';
|
||
|
||
// 4. Format date and prep payload
|
||
let dbFormat = convertDBFormatted(feFormat);
|
||
|
||
try {
|
||
const payload = {
|
||
lead_id: document.getElementById('act_lead_id').value,
|
||
activity_type: selectedType,
|
||
notes: actNotes, // Reused the trimmed variable from above
|
||
scheduled_date: dbFormat,
|
||
assigned_to: actOwner, // Reused the trimmed variable from above
|
||
status: 'pending',
|
||
additional_assigned_ids : Array.from(document.getElementById('act_multiple_owner').selectedOptions).map(o => o.value)
|
||
};
|
||
|
||
const res = await fetch(`${API}/activities`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
if (res.ok) {
|
||
toastr.success('Activity Created Successfully');
|
||
closeModal('activityModal');
|
||
if (flag === "frompopup") {
|
||
viewDetail(actLead);
|
||
} else {
|
||
window.location.reload();
|
||
}
|
||
} 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();
|
||
|
||
let flag = document.getElementById('string_flag').value; //"frompopup"
|
||
|
||
// 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();
|
||
|
||
const hasActiveType = document.querySelector('#f_typeButtons .f_activity_type.active');
|
||
if (!hasActiveType) {
|
||
toastr.warning('Please select the next activity type.');
|
||
return;
|
||
}
|
||
// FIX: Check your global variable, not the undefined DOM element
|
||
if (!selectedFollowUpActivityType) {
|
||
toastr.warning('Please select the next activity type.');
|
||
return;
|
||
}
|
||
|
||
if (!fNotes) {
|
||
toastr.warning('Follow-up notes is required');
|
||
return;
|
||
}
|
||
|
||
if (!fDate) {
|
||
toastr.warning('Scheduled date & time is required');
|
||
return;
|
||
}
|
||
|
||
if (!fAssigned) {
|
||
toastr.warning('Please Select the user to be Assigned');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// --- ALL VALIDATION PASSED: Now disable button ---
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerText = 'Submitting...';
|
||
|
||
try {
|
||
const payload = {
|
||
completion_notes: compNotes, // Use the already trimmed variable
|
||
create_followup: createFollowup,
|
||
};
|
||
|
||
// Mark current activity complete
|
||
const res = await fetch(`${API}/activities/${activityId}/complete`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
if (res.ok) {
|
||
// Handle Follow-up logic
|
||
if (createFollowup) {
|
||
let feFormat = document.getElementById('f_date').value;
|
||
let dbFormat = convertDBFormatted(feFormat);
|
||
|
||
const payload2 = {
|
||
lead_id: leadId,
|
||
activity_type: selectedFollowUpActivityType,
|
||
notes: document.getElementById('f_notes').value,
|
||
scheduled_date: dbFormat,
|
||
assigned_to: document.getElementById('f_assigned_to').value,
|
||
status: 'pending',
|
||
additional_assigned_ids : Array.from(document.getElementById('f_multiple_owner').selectedOptions).map(o => o.value)
|
||
};
|
||
|
||
const res2 = await fetch(`${API}/activities`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload2)
|
||
});
|
||
|
||
if (!res2.ok) {
|
||
console.error("Follow-up failed to save");
|
||
} else {
|
||
console.log("Follow-up Success to save");
|
||
}
|
||
}
|
||
|
||
toastr.success('Updated Successfully');
|
||
|
||
// Final Actions
|
||
closeModal('completeModal');
|
||
|
||
if (flag === "frompopup") {
|
||
viewDetail(leadId);
|
||
} else if (flag === "frommain") {
|
||
window.location.reload();
|
||
}
|
||
|
||
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
|
||
}
|
||
});
|
||
|
||
function convertDBFormatted(input) {
|
||
|
||
if (!input) return null;
|
||
|
||
let parts = input.trim().split(' ');
|
||
if (parts.length !== 3) return null;
|
||
|
||
let [datePart, timePart, ampm] = parts;
|
||
|
||
let [day, month, year] = datePart.split('/');
|
||
let [hours, minutes, seconds] = timePart.split(':');
|
||
|
||
hours = parseInt(hours, 10);
|
||
|
||
// Convert 12-hour to 24-hour
|
||
if (ampm.toUpperCase() === "PM" && hours < 12) hours += 12;
|
||
if (ampm.toUpperCase() === "AM" && hours === 12) hours = 0;
|
||
|
||
return (
|
||
year + '-' +
|
||
month.padStart(2, '0') + '-' +
|
||
day.padStart(2, '0') + ' ' +
|
||
String(hours).padStart(2, '0') + ':' +
|
||
minutes.padStart(2, '0') + ':' +
|
||
seconds.padStart(2, '0')
|
||
);
|
||
}
|
||
|
||
function csvEscape(value) {
|
||
const text = String(value ?? '').replace(/"/g, '""');
|
||
return `"${text}"`;
|
||
}
|
||
|
||
function formatIndianDate(value) {
|
||
if (!value) return '';
|
||
const date = new Date(String(value).replace(' ', 'T'));
|
||
if (Number.isNaN(date.getTime())) return value;
|
||
|
||
return date.toLocaleString('en-IN', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: true
|
||
}).replace(',', '').toUpperCase();
|
||
}
|
||
|
||
function downloadCsv(filename, rows) {
|
||
const csv = rows.map(row => row.map(csvEscape).join(',')).join('\n');
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = filename;
|
||
link.click();
|
||
URL.revokeObjectURL(link.href);
|
||
}
|
||
|
||
async function exportActivitiesCsv() {
|
||
const selectedMemberIds = getSelectedMemberIds(salesManagerWithHeadIds);
|
||
if (selectedMemberIds.length === 0) return toastr.warning('No members available to export');
|
||
|
||
const params = new URLSearchParams({
|
||
status: filter === 'all' ? '' : filter,
|
||
search: document.getElementById('mainSearch')?.value || '',
|
||
limit: 10000,
|
||
offset: 0,
|
||
assigned_to: selectedMemberIds.join(','),
|
||
financial_year: getSelectedFinancialYear()
|
||
});
|
||
|
||
const res = await fetch(`${API}/activities?${params.toString()}`);
|
||
const json = await res.json();
|
||
const rows = [
|
||
['Company', 'Activity Type', 'Status', 'Assigned To', 'Additional Members', 'Scheduled Date', 'Created At', 'Notes'],
|
||
...(json.data || []).map(a => [
|
||
a.company_name || '',
|
||
a.activity_type || '',
|
||
capitalize(a.status),
|
||
a.assigned_to_name || '',
|
||
a.additional_assigned_names || '',
|
||
formatIndianDate(a.scheduled_date),
|
||
formatIndianDate(a.created_at),
|
||
a.notes || a.completion_notes || ''
|
||
])
|
||
];
|
||
downloadCsv(`sales-activities-${getSelectedFinancialYear()}.csv`, rows);
|
||
}
|
||
|
||
initSalesToolbarFilters();
|
||
fetchActivities();
|
||
|
||
</script>
|