nhance/app/Views/sales/activity_view.php
2026-05-12 11:23:19 +05:30

1463 lines
68 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<style>
/* POC Exact Styling */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.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, '&quot;');
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>