nhance/app/Views/sales/target_view.php
2026-02-27 15:38:58 +05:30

602 lines
26 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>
* { 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 */
}
}
@keyframes fadeHighlight {
0% { background-color: #fffbcc; }
100% { background-color: transparent; }
}
.row-highlight {
animation: fadeHighlight 2s ease forwards;
}
</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>Sales Manager</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);
records.sort((a, b) => {
let startYearA = parseInt(a.fy_year.split('-')[0], 10);
let startYearB = parseInt(b.fy_year.split('-')[0], 10);
return startYearB - startYearA;
});
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();
await renderDetailTable(userId);
const savedId = result.data?.id || recordId;
const targetRow = document.getElementById(`row_${savedId}`);
if (targetRow) {
targetRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetRow.classList.add('row-highlight');
setTimeout(() => targetRow.classList.remove('row-highlight'), 3000);
}
} 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>