FIX_salesTracker8
This commit is contained in:
parent
0037133f8a
commit
3cdd73a7ee
@ -914,7 +914,7 @@ $routes->group('sales', function($routes) {
|
||||
$routes->get('/', 'SalesController::index');
|
||||
|
||||
$routes->get('loadactivities', 'SalesController::loadactivities');
|
||||
|
||||
|
||||
$routes->get('loadtargets', 'SalesController::loadtargets');
|
||||
|
||||
$routes->get('page/(:segment)', 'SalesController::noPage/$1');
|
||||
@ -997,6 +997,13 @@ $routes->group('sales', function($routes) {
|
||||
// Delete note
|
||||
$routes->delete('notes/(:num)', 'SalesController::deleteNote/$1');
|
||||
|
||||
// ==================== TARGETS ROUTES ====================
|
||||
$routes->get('targets', 'SalesController::getTargets');
|
||||
$routes->get('targets/user/(:num)', 'SalesController::getTargetByUser/$1');
|
||||
$routes->get('targets/fy/(:segment)','SalesController::getTargetByFY/$1');
|
||||
$routes->post('targets', 'SalesController::createTarget');
|
||||
$routes->put('targets/(:num)', 'SalesController::updateTarget/$1');
|
||||
$routes->delete('targets/(:num)', 'SalesController::deleteTarget/$1');
|
||||
|
||||
//Dashboard
|
||||
$routes->get('dashboard', 'SalesController::dashboard');
|
||||
|
||||
@ -7,6 +7,7 @@ use App\Models\SalesActualLeadModel;
|
||||
use App\Models\SalesContactPersonModel;
|
||||
use App\Models\SalesActivityModel;
|
||||
use App\Models\SalesLeadNoteModel;
|
||||
use App\Models\SalesTargetModel;
|
||||
use App\Models\UserModel;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
@ -20,6 +21,8 @@ class SalesController extends BaseController
|
||||
protected $activityModel;
|
||||
protected $noteModel;
|
||||
protected $userModel;
|
||||
protected $targetModel;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@ -28,6 +31,7 @@ class SalesController extends BaseController
|
||||
$this->activityModel = new SalesActivityModel();
|
||||
$this->noteModel = new SalesLeadNoteModel();
|
||||
$this->userModel = new UserModel();
|
||||
$this->targetModel = new SalesTargetModel();
|
||||
}
|
||||
|
||||
|
||||
@ -760,6 +764,138 @@ class SalesController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SALES TARGET APIs ====================
|
||||
/**
|
||||
* Get all sales targets
|
||||
* GET /api/sales/targets
|
||||
*/
|
||||
public function getTargets()
|
||||
{
|
||||
try {
|
||||
$targets = $this->targetModel->findAll();
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'data' => $targets
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sales target by user
|
||||
* GET /api/sales/targets/user/{userId}
|
||||
*/
|
||||
public function getTargetByUser($userId)
|
||||
{
|
||||
try {
|
||||
$targets = $this->targetModel->where('user_id', $userId)->findAll();
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'data' => $targets
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sales target by FY year
|
||||
* GET /api/sales/targets/fy/{fyYear}
|
||||
*/
|
||||
public function getTargetByFY($fyYear)
|
||||
{
|
||||
try {
|
||||
$targets = $this->targetModel->where('fy_year', $fyYear)->findAll();
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'data' => $targets
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sales target
|
||||
* POST /api/sales/targets
|
||||
*/
|
||||
public function createTarget()
|
||||
{
|
||||
try {
|
||||
$data = $this->request->getJSON(true);
|
||||
$data['created_by'] = $this->getUserId();
|
||||
$data['updated_by'] = $this->getUserId();
|
||||
|
||||
if (!$this->targetModel->insert($data)) {
|
||||
return $this->fail($this->targetModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$targetId = $this->targetModel->getInsertID();
|
||||
$target = $this->targetModel->find((int)$targetId);
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => 'Sales target created successfully',
|
||||
'data' => $target
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sales target
|
||||
* PUT /api/sales/targets/{id}
|
||||
*/
|
||||
public function updateTarget($id)
|
||||
{
|
||||
try {
|
||||
$target = $this->targetModel->find((int)$id);
|
||||
if (!$target) {
|
||||
return $this->failNotFound('Sales target not found');
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
$data['updated_by'] = $this->getUserId();
|
||||
|
||||
if (!$this->targetModel->update($id, $data)) {
|
||||
return $this->fail($this->targetModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$updatedTarget = $this->targetModel->find((int)$id);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Sales target updated successfully',
|
||||
'data' => $updatedTarget
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete sales target
|
||||
* DELETE /api/sales/targets/{id}
|
||||
*/
|
||||
public function deleteTarget($id)
|
||||
{
|
||||
try {
|
||||
$target = $this->targetModel->find((int)$id);
|
||||
if (!$target) {
|
||||
return $this->failNotFound('Sales target not found');
|
||||
}
|
||||
|
||||
$this->targetModel->delete((int)$id);
|
||||
return $this->respondDeleted([
|
||||
'status' => 'success',
|
||||
'message' => 'Sales target deleted successfully'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== HELPER METHODS ====================
|
||||
|
||||
/**
|
||||
|
||||
38
app/Models/SalesTargetModel.php
Normal file
38
app/Models/SalesTargetModel.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class SalesTargetModel extends Model
|
||||
{
|
||||
protected $table = 'sales_target';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $allowedFields = [
|
||||
'user_id',
|
||||
'fy_year',
|
||||
'target_amount',
|
||||
'created_by',
|
||||
'updated_by'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
protected $validationRules = [
|
||||
'user_id' => 'required|integer',
|
||||
'fy_year' => 'required|max_length[9]',
|
||||
'target_amount' => 'required|decimal',
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'user_id' => ['required' => 'User is required'],
|
||||
'fy_year' => ['required' => 'Financial year is required'],
|
||||
'target_amount' => ['required' => 'Target amount is required', 'decimal' => 'Target amount must be a valid number'],
|
||||
];
|
||||
}
|
||||
581
app/Views/sales/target_view.php
Normal file
581
app/Views/sales/target_view.php
Normal file
@ -0,0 +1,581 @@
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
LAYOUT — Page Shell
|
||||
═══════════════════════════════════════════ */
|
||||
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between; /* pushes subtitle left, search right */
|
||||
align-items: center; /* vertically centers both */
|
||||
padding: 14px 28px;
|
||||
background: white;
|
||||
/* border-bottom: 1px solid #e0e0e0; */
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
SEARCH INPUT
|
||||
═══════════════════════════════════════════ */
|
||||
.search-input { padding: 9px 14px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; width: 240px; outline: none; background: #f5f5f5; transition: border-color .2s;}
|
||||
.search-input:focus { border-color: #ff6b35; }
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
CONTENT AREA
|
||||
═══════════════════════════════════════════ */
|
||||
.content-area { width: 100%; }
|
||||
.content { padding: 10px; width: 100%; max-width: 100%;}
|
||||
.subtitle { font-size: 13px; color: #999; margin-bottom: 20px; }
|
||||
.view-container { display: none; }
|
||||
.view-container.active { display: block; }
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
MAIN TABLE
|
||||
═══════════════════════════════════════════ */
|
||||
.tt-table-wrapper { border: 1px solid #e0e0e0; border-radius: 10px; overflow: hidden; width: 100%;}
|
||||
.tt-main-table { width: 100%;border-collapse: collapse;table-layout: fixed;}
|
||||
.tt-main-table thead { display: table; width: 100%; table-layout: fixed; }
|
||||
.tt-main-table thead th { background: #f4f4f4; padding: 11px 14px; text-align: left;font-size: 12px;font-weight: 700;color: #777;text-transform: uppercase;letter-spacing: .4px;border-bottom: 1px solid #e0e0e0;}
|
||||
.tt-main-table tbody { display: block; max-height: 530px; overflow-y: auto;}
|
||||
.tt-main-table tbody tr { display: table; width: 100%; table-layout: fixed; }
|
||||
.tt-main-table tbody td { padding: 12px 14px; font-size: 14px; border-bottom: 1px solid #f0f0f0; vertical-align: middle;}
|
||||
.tt-main-table tbody tr:last-child td { border-bottom: none; }
|
||||
.tt-main-table tbody tr:hover td { background: #fafafa; }
|
||||
/* Empty row — must stay as table-row (not display:table override) */
|
||||
.tt-main-table tbody tr.empty-row { display: table-row; width: 100%; }
|
||||
.tt-main-table tbody tr.empty-row td { width: 100%; text-align: center; }
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
MEMBER AVATAR (used inside main table)
|
||||
═══════════════════════════════════════════ */
|
||||
.tt-person-cell { display: flex; align-items: center; gap: 1px;}
|
||||
/* .tt-avatar { width: 38px; height: 38px; border-radius: 50%; background: #ff6b35; color: white; font-size: 14px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0;} */
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
EMPTY STATE
|
||||
═══════════════════════════════════════════ */
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; color: #aaa; gap: 10px; }
|
||||
.empty-icon { font-size: 36px; opacity: 0.5; line-height: 1;}
|
||||
.empty-state p { margin: 0; font-size: 14px; font-weight: 500; color: #bbb;}
|
||||
.empty-row td { padding: 0 !important; border: none !important; background: transparent !important;}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
MODAL — Overlay & Box
|
||||
═══════════════════════════════════════════ */
|
||||
.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-box { background: white; border-radius: 14px; width: 92%; max-width: 640px; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 8px 32px rgba(0, 0, 0, .18); animation: slideUp .2s ease;}
|
||||
.modal-header { padding: 6px 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; }
|
||||
.modal-body { padding: 25px; flex: 1; overflow-y: auto;}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
MODAL — Form Fields
|
||||
═══════════════════════════════════════════ */
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px;}
|
||||
.form-group { margin-bottom: 20px; }
|
||||
.form-label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; color: #333;}
|
||||
|
||||
.form-input,
|
||||
.form-textarea,
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
background: #f5f5f5; /* grey background */
|
||||
color: #333;
|
||||
transition: border-color .2s, background .2s;
|
||||
}
|
||||
|
||||
.form-textarea { resize: vertical; min-height: 100px;}
|
||||
|
||||
.form-input:focus,
|
||||
.form-textarea:focus,
|
||||
.form-select:focus { outline: none; border-color: #ff6b35; background: #fff;}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
MODAL — Detail Table (targets list)
|
||||
═══════════════════════════════════════════ */
|
||||
.table-wrap { border: 1px solid #e0e0e0; border-radius: 10px; overflow: hidden; margin-top: 16px;}
|
||||
.table-wrap table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||||
.table-wrap thead { display: table; width: 100%; table-layout: fixed; }
|
||||
.table-wrap thead th { background: #f8f8f8; padding: 12px 16px; text-align: left; font-size: 13px; font-weight: 600; color: #666; border-bottom: 1px solid #e0e0e0;}
|
||||
.table-wrap tbody { display: block; max-height: 220px; overflow-y: auto; }
|
||||
.table-wrap tbody tr { display: table; width: 100%; table-layout: fixed; }
|
||||
.table-wrap tbody td { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; font-size: 14px; color: #333; vertical-align: middle;}
|
||||
.table-wrap tbody tr:last-child td { border-bottom: none; }
|
||||
.table-wrap tbody tr:hover td { background: #fafafa; }
|
||||
/* Empty row fix for modal table */
|
||||
.table-wrap tbody tr.empty-row { display: table-row; }
|
||||
.table-wrap tbody tr.empty-row td { padding: 0 !important; border: none !important; text-align: center;}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
BUTTONS
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
/* Primary action — View Target (main table) */
|
||||
.btn-tt-view {
|
||||
background: #ff6b35;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 7px 16px;
|
||||
border-radius: 7px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background .2s;
|
||||
}
|
||||
.btn-tt-view:hover { background: #ff5722; }
|
||||
|
||||
/* Close button — modal header */
|
||||
.close-btn {
|
||||
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;
|
||||
}
|
||||
.close-btn:hover { background: #f0f0f0; color: #333; }
|
||||
|
||||
/* Edit row — modal detail table */
|
||||
.btn-edit {
|
||||
background: #e8f4ff;
|
||||
color: #1976d2;
|
||||
border: none;
|
||||
padding: 5px 12px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
transition: background .2s;
|
||||
}
|
||||
.btn-edit:hover { background: #bbdefb; }
|
||||
|
||||
/* Remove row — modal detail table */
|
||||
.btn-remove {
|
||||
background: #ffeaea;
|
||||
color: #d32f2f;
|
||||
border: none;
|
||||
padding: 5px 12px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: background .2s;
|
||||
}
|
||||
.btn-remove:hover { background: #ffcdd2; }
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
ANIMATIONS
|
||||
═══════════════════════════════════════════ */
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
RESPONSIVE
|
||||
═══════════════════════════════════════════ */
|
||||
@media (max-width: 540px) {
|
||||
.form-row { grid-template-columns: 1fr; }
|
||||
.search-input { width: 100%; }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
GREY BOX — Input Section Container
|
||||
═══════════════════════════════════════════ */
|
||||
.grey-box {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.grey-box .form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto; /* 2 fields + button */
|
||||
gap: 15px;
|
||||
align-items: flex-end; /* aligns button to bottom of fields */
|
||||
}
|
||||
|
||||
.grey-box .form-group {
|
||||
margin-bottom: 0; /* remove default gap inside grey box */
|
||||
}
|
||||
|
||||
.grey-box .btn {
|
||||
white-space: nowrap;
|
||||
height: 40px; /* match input height */
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.grey-box .form-row {
|
||||
grid-template-columns: 1fr; /* stack vertically on mobile */
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="main-content">
|
||||
<hr class="my-0">
|
||||
<div class="top-bar">
|
||||
<p class="subtitle" id="memberCount"></p>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Search member…" oninput="filterMembers(this.value)">
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="content">
|
||||
<div class="content-area view-container active" id="teamtargetView">
|
||||
<div class="tt-table-wrapper">
|
||||
<table class="tt-main-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Person</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ttMainTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal" id="targetModal">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="modalTitle">–</span>
|
||||
<button class="close-btn" onclick="closeModal()" title="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="recordId">
|
||||
<input type="hidden" id="currentUserId">
|
||||
|
||||
<div class="grey-box">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Financial Year</label>
|
||||
<select class="form-select" id="fyYear"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Amount (₹)</label>
|
||||
<input type="text" class="form-input" id="targetAmount" placeholder="e.g. 500000"
|
||||
oninput="this.value=this.value.replace(/[^0-9]/g,'')">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="button" class="btn btn-primary waves-effect btn-sm w-100" id="btn-save" onclick="saveRecord()">Save Target</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Financial Year</th>
|
||||
<th>Amount</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="detailTableBody">
|
||||
<tr class="empty-row"><td colspan="3">No targets added yet.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const users = <?= json_encode($users ?? []) ?>;
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
DATA
|
||||
────────────────────────────────────────── */
|
||||
const teamMembers = users.map(user => ({
|
||||
id: user.id,
|
||||
name: user.first_name + ' ' + user.last_name
|
||||
}));
|
||||
const API = '<?= base_url('sales') ?>';
|
||||
|
||||
let activeUserId = null;
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
RENDER MEMBER LIST
|
||||
────────────────────────────────────────── */
|
||||
function renderMemberList(filter = '') {
|
||||
const tbody = document.getElementById('ttMainTableBody');
|
||||
const countEl = document.getElementById('memberCount');
|
||||
const term = filter.trim().toLowerCase();
|
||||
|
||||
const filtered = teamMembers.filter(m =>
|
||||
m.name.toLowerCase().includes(term)
|
||||
);
|
||||
|
||||
|
||||
// countEl.textContent = `${filtered.length} member${filtered.length !== 1 ? 's' : ''}`;
|
||||
countEl.textContent = ``;
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr class="empty-row">
|
||||
<td colspan="2">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">👤</div>
|
||||
<p>No Member found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(m => {
|
||||
// const initials = m.name.split(' ').map(w => w[0]).join('').substring(0, 2).toUpperCase();
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td id="card_${m.id}">
|
||||
<div class="tt-person-cell">
|
||||
<div class="avatar-md">
|
||||
<div class="avatar-title bg-soft-primary rounded-circle text-primary font-20 font-weight-semibold"
|
||||
style="background-color: #f1f3fa; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center;">
|
||||
${(m.name || '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<span>${m.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-primary waves-effect waves-light" onclick="openModal(${m.id})">View Target</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function filterMembers(val) {
|
||||
renderMemberList(val);
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
FY OPTIONS
|
||||
────────────────────────────────────────── */
|
||||
function populateFY(selectedFY = null) {
|
||||
const sel = document.getElementById('fyYear');
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = today.getMonth(); // 0-11
|
||||
|
||||
// Determine current FY start year
|
||||
let currentFYStart;
|
||||
|
||||
if (month >= 3) { // April (3) or later
|
||||
currentFYStart = year;
|
||||
} else { // Jan, Feb, Mar
|
||||
currentFYStart = year - 1;
|
||||
}
|
||||
|
||||
sel.innerHTML = '';
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const startYear = currentFYStart - i;
|
||||
const label = `${startYear}-${startYear + 1}`;
|
||||
|
||||
const opt = document.createElement('option');
|
||||
opt.value = label;
|
||||
opt.textContent = label;
|
||||
|
||||
if (label === selectedFY) opt.selected = true;
|
||||
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
MODAL
|
||||
────────────────────────────────────────── */
|
||||
function openModal(userId) {
|
||||
|
||||
activeUserId = userId;
|
||||
|
||||
const member = teamMembers.find(m => Number(m.id) === Number(userId));
|
||||
|
||||
document.getElementById('modalTitle').textContent = member ? member.name : '–';
|
||||
document.getElementById('currentUserId').value = userId;
|
||||
document.getElementById('recordId').value = '';
|
||||
document.getElementById('targetAmount').value = '';
|
||||
populateFY();
|
||||
renderDetailTable(userId);
|
||||
document.getElementById('targetModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('targetModal').classList.remove('active');
|
||||
activeUserId = null;
|
||||
}
|
||||
|
||||
document.getElementById('targetModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
RENDER DETAIL TABLE
|
||||
────────────────────────────────────────── */
|
||||
async function renderDetailTable(userId) {
|
||||
const tbody = document.getElementById('detailTableBody');
|
||||
tbody.innerHTML = '<tr><td colspan="3">Loading...</td></tr>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/targets/user/${userId}`);
|
||||
const json = await res.json();
|
||||
const records = json.data || [];
|
||||
|
||||
if (records.length === 0) {
|
||||
tbody.innerHTML = '<tr class="empty-row"><td colspan="3">No targets added yet.</td></tr>';
|
||||
return;
|
||||
}
|
||||
records.sort((a, b) => b.id - a.id);
|
||||
tbody.innerHTML = records.map(r => `
|
||||
<tr id="row_${r.id}">
|
||||
<td>${r.fy_year}</td>
|
||||
<td>₹ ${formatNum(r.target_amount)}</td>
|
||||
<td>
|
||||
<button class="btn-edit" onclick="editRecord(${r.id}, '${r.fy_year}', ${r.target_amount})">Edit</button>
|
||||
<button class="btn-remove" onclick="removeRecord(${r.id})">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} catch (err) {
|
||||
tbody.innerHTML = '<tr><td colspan="3">Failed to load targets.</td></tr>';
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
SAVE (INSERT / UPDATE)
|
||||
────────────────────────────────────────── */
|
||||
async function saveRecord() {
|
||||
const userId = activeUserId;
|
||||
const recordId = document.getElementById('recordId').value.trim();
|
||||
const fyYear = document.getElementById('fyYear').value;
|
||||
const amtRaw = document.getElementById('targetAmount').value.trim();
|
||||
|
||||
// Validation
|
||||
if (!fyYear) { toastr.error('Please select a Financial Year.'); return; }
|
||||
if (!amtRaw) { toastr.error('Please enter an amount.'); return; }
|
||||
const amount = parseInt(amtRaw, 10);
|
||||
if (isNaN(amount) || amount <= 0) { toastr.error('Enter a valid positive amount.'); return; }
|
||||
|
||||
const existingRows = document.querySelectorAll('#detailTableBody tr[id^="row_"]');
|
||||
for (const row of existingRows) {
|
||||
const rowId = row.id.replace('row_', '');
|
||||
const rowFY = row.cells[0].textContent.trim();
|
||||
// Skip the row being edited
|
||||
if (recordId && rowId === recordId) continue;
|
||||
if (rowFY === fyYear) {
|
||||
toastr.error(`A target for ${fyYear} already exists.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const saveBtn = document.getElementById('btn-save');
|
||||
const originalBtnText = saveBtn.innerText;
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerText = recordId ? 'Updating...' : 'Saving...';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
user_id: userId,
|
||||
fy_year: fyYear,
|
||||
target_amount: amount
|
||||
};
|
||||
|
||||
const isUpdate = !!recordId;
|
||||
const url = isUpdate ? `${API}/targets/${recordId}` : `${API}/targets`;
|
||||
const method = isUpdate ? 'PUT' : 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(isUpdate ? 'Target updated!' : 'Target saved!');
|
||||
// Reset form
|
||||
document.getElementById('recordId').value = '';
|
||||
document.getElementById('targetAmount').value = '';
|
||||
populateFY();
|
||||
renderDetailTable(userId);
|
||||
} else {
|
||||
toastr.error(result.message || 'Failed to save target.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toastr.error('A network error occurred.');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerText = originalBtnText;
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
EDIT — populate form from table row data
|
||||
────────────────────────────────────────── */
|
||||
function editRecord(id, fyYear, amount) {
|
||||
document.getElementById('recordId').value = id;
|
||||
document.getElementById('targetAmount').value = amount;
|
||||
populateFY(fyYear);
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
REMOVE
|
||||
────────────────────────────────────────── */
|
||||
async function removeRecord(id) {
|
||||
if (!confirm('Are you sure you want to remove this target?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/targets/${id}`, { method: 'DELETE' });
|
||||
const result = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success('Target removed.');
|
||||
renderDetailTable(activeUserId);
|
||||
} else {
|
||||
toastr.error(result.message || 'Failed to remove target.');
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toastr.error('A network error occurred.');
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
HELPERS
|
||||
────────────────────────────────────────── */
|
||||
function formatNum(n) {
|
||||
return Number(n).toLocaleString('en-IN');
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
INIT
|
||||
────────────────────────────────────────── */
|
||||
renderMemberList();
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user