1106 lines
60 KiB
PHP
1106 lines
60 KiB
PHP
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||
|
||
.dash-container {
|
||
padding: 28px;
|
||
background: #f4f6fb;
|
||
font-family: 'Plus Jakarta Sans', 'Segoe UI', sans-serif;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* ── Dashboard Header ── */
|
||
.dash-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.page-title { font-size: 20px; font-weight: 800; color: #0f172a; margin: 0 0 4px 0; }
|
||
.page-sub { font-size: 13px; color: #7c8db0; font-weight: 600; }
|
||
.fy-select-wrap select {
|
||
background: #fff;
|
||
padding: 10px 16px;
|
||
border-radius: 10px;
|
||
border: 1px solid #e4eaf5;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
cursor: pointer;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||
min-width: 200px;
|
||
appearance: none;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%237c8db0' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat;
|
||
background-position: right 14px center;
|
||
padding-right: 36px;
|
||
}
|
||
.fy-select-wrap select:focus { outline: none; border-color: #4f46e5; }
|
||
|
||
/* ── Stat Cards ── */
|
||
.stat-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 18px;
|
||
margin-bottom: 24px;
|
||
}
|
||
.card-hero {
|
||
background: white;
|
||
padding: 22px 24px;
|
||
border-radius: 14px;
|
||
border: 1px solid #e8edf5;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
.card-hero::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0; left: 0;
|
||
width: 100%; height: 4px;
|
||
}
|
||
.card-hero:nth-child(1)::before { background: linear-gradient(90deg, #667eea, #764ba2); }
|
||
.card-hero:nth-child(2)::before { background: linear-gradient(90deg, #f093fb, #f5576c); }
|
||
.card-hero:nth-child(3)::before { background: linear-gradient(90deg, #fda085, #f6d365); }
|
||
.card-hero:nth-child(4)::before { background: linear-gradient(90deg, #43e97b, #38f9d7); }
|
||
.card-hero:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.09); }
|
||
.card-hero.stat-link { cursor: pointer; }
|
||
.stat-icon { width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; margin-bottom: 14px; }
|
||
.stat-val { font-size: 30px; font-weight: 800; color: #1a202c; line-height: 1; }
|
||
.stat-label { color: #8896aa; font-size: 13px; margin-top: 6px; font-weight: 600; }
|
||
|
||
/* ── Shared Card ── */
|
||
.card { background: white; padding: 24px; border-radius: 14px; border: 1px solid #e8edf5; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
|
||
|
||
/* ── Main Grid ── */
|
||
.main-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 24px; align-items: start; }
|
||
.main-grid > .card { height: 520px; display: flex; flex-direction: column; }
|
||
.table-scroll-area { flex: 1; overflow-y: auto; }
|
||
.table-scroll-area thead th { position: sticky; top: 0; background: white; z-index: 1; }
|
||
|
||
/* ── Right Panel ── */
|
||
.right-panel { height: 520px; background: white; border-radius: 14px; border: 1px solid #e8edf5; box-shadow: 0 2px 8px rgba(0,0,0,0.04); padding: 22px; box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; }
|
||
.team-members-scroll { flex: 1; overflow-y: auto; margin-top: 10px; }
|
||
.team-member { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid #f4f6fb; }
|
||
.team-member:last-child { border-bottom: none; }
|
||
.member-img { width: 38px; height: 38px; border-radius: 50%; color: white; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 15px; flex-shrink: 0; }
|
||
.act-stats { text-align: right; }
|
||
.act-stats .total { font-weight: 700; font-size: 13px; color: #1a202c; }
|
||
.act-stats .done { font-size: 11px; color: #38a169; font-weight: 600; margin-top: 2px; }
|
||
|
||
/* ── Activity Breakdown ── */
|
||
.breakdown-section { flex-shrink: 0; margin-top: 12px; border: 1px solid #f0f4fa; border-radius: 12px; padding: 14px; background: #fafbfd; }
|
||
.breakdown-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 9px; font-size: 13px; }
|
||
.breakdown-item:last-child { margin-bottom: 0; }
|
||
.breakdown-bar-wrap { flex: 1; height: 5px; background: #eef0f5; border-radius: 99px; margin: 0 10px; overflow: hidden; }
|
||
.breakdown-bar { height: 100%; border-radius: 99px; }
|
||
|
||
/* ── Table ── */
|
||
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||
table { width: 100%; border-collapse: collapse; }
|
||
th { text-align: left; padding: 10px 12px; color: #8896aa; font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; border-bottom: 1px solid #edf2f7; background: white; font-weight: 700; }
|
||
td { padding: 13px 12px; border-bottom: 1px solid #f4f6fb; font-size: 14px; vertical-align: middle; }
|
||
tbody tr:hover { background: #fafbfe; }
|
||
.won-opportunity-row { cursor: pointer; }
|
||
.won-opportunity-row:hover { background: #eff6ff !important; }
|
||
|
||
/* ── Status Pills ── */
|
||
.status-pill { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; }
|
||
.status-completed { background: #f0fff4; color: #38a169; }
|
||
.status-pending { background: #fffaf0; color: #dd6b20; }
|
||
.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; }
|
||
.leads-table-wrap { max-height: 480px; overflow-y: auto; }
|
||
.leads-table-wrap thead th { position: sticky; top: 0; background: white; z-index: 1; }
|
||
.activity-count-link { display: inline-block; color: #2563eb; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
||
.activity-count-muted { display: inline-block; color: #8896aa; cursor: help; }
|
||
.empty-state { text-align: center; padding: 50px 20px; color: #aaa; }
|
||
.empty-icon { width: 70px; height: 70px; margin: 0 auto 16px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32px; }
|
||
|
||
/* ── Responsive ── */
|
||
@media (max-width: 1200px) { .stat-cards { grid-template-columns: repeat(2, 1fr); } }
|
||
@media (max-width: 992px) { .main-grid { grid-template-columns: 1fr; } .main-grid > .card, .right-panel { height: 450px; } .stat-cards { grid-template-columns: repeat(2, 1fr); } }
|
||
@media (max-width: 576px) { .dash-container { padding: 14px; } .stat-cards { grid-template-columns: 1fr 1fr; gap: 10px; } .stat-val { font-size: 24px; } .main-grid > .card, .right-panel { height: 400px; } table th, table td { padding: 10px 8px; font-size: 12px; } .dash-header { flex-direction: column; align-items: flex-start; } }
|
||
|
||
/* ══ TEAM ACHIEVEMENT SECTION ══ */
|
||
.section-head { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 16px; }
|
||
.section-title { font-size: 16px; font-weight: 800; color: #0f172a; }
|
||
.section-sub { font-size: 12px; color: #7c8db0; font-weight: 600; margin-top: 3px; }
|
||
.branch-summary { text-align: right; }
|
||
.branch-amt { font-size: 20px; font-weight: 800; color: #0f172a; }
|
||
.branch-bar-wrap { width: 180px; height: 5px; background: #e4eaf5; border-radius: 99px; overflow: hidden; margin: 6px 0 4px auto; }
|
||
.branch-bar-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg,#4f46e5,#818cf8); }
|
||
.branch-pct-lbl { font-size: 11px; color: #4f46e5; font-weight: 700; }
|
||
.achieve-list { background: #fff; border-radius: 14px; border: 1px solid #e4eaf5; box-shadow: 0 2px 12px rgba(15,23,42,0.06); overflow: hidden; }
|
||
.list-col-head { display: grid; grid-template-columns: 260px 1fr 130px 130px 130px 110px 80px; padding: 10px 22px; background: #f8fafd; border-bottom: 1px solid #e4eaf5; }
|
||
.col-lbl { font-size: 10px; font-weight: 700; color: #7c8db0; letter-spacing: .07em; text-transform: uppercase; display: flex; align-items: center; }
|
||
.col-lbl.right { justify-content: flex-end; }
|
||
.col-lbl.center { justify-content: center; }
|
||
.achieve-row { display: grid; grid-template-columns: 260px 1fr 130px 130px 130px 110px 80px; padding: 16px 22px; border-bottom: 1px solid #f1f5f9; align-items: center; cursor: pointer; transition: background .15s; position: relative; }
|
||
.achieve-row:last-child { border-bottom: none; }
|
||
.achieve-row:hover { background: #f8fafd; }
|
||
.achieve-row:hover .row-arrow { opacity: 1; transform: translateY(-50%) translateX(0); }
|
||
.member-cell { display: flex; align-items: center; gap: 12px; }
|
||
.m-avatar { width: 42px; height: 42px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 16px; color: #fff; flex-shrink: 0; }
|
||
.m-name { font-weight: 700; font-size: 14px; color: #0f172a; }
|
||
.m-role { font-size: 11px; color: #7c8db0; font-weight: 600; margin-top: 2px; }
|
||
.prog-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; }
|
||
.prog-pct { font-size: 13px; font-weight: 800; }
|
||
.prog-amts { font-size: 11px; color: #7c8db0; font-weight: 600; }
|
||
.prog-bar-wrap { height: 6px; background: #f0f3fa; border-radius: 99px; overflow: hidden; }
|
||
.prog-bar { height: 100%; border-radius: 99px; }
|
||
.num-cell { text-align: right; }
|
||
.num-val { font-size: 15px; font-weight: 800; }
|
||
.num-lbl { font-size: 10px; color: #7c8db0; font-weight: 700; margin-top: 2px; letter-spacing: .04em; }
|
||
.num-cell.center-col { text-align: center; }
|
||
.badge { display: inline-flex; align-items: center; gap: 5px; padding: 4px 12px; border-radius: 99px; font-size: 11px; font-weight: 700; white-space: nowrap; }
|
||
.b-achieved { background: #ecfdf5; color: #059669; }
|
||
.b-ontrack { background: #eff6ff; color: #2563eb; }
|
||
.b-behind { background: #fff7ed; color: #c2410c; }
|
||
.b-atrisk { background: #fef2f2; color: #b91c1c; }
|
||
.row-arrow { position: absolute; right: 18px; top: 50%; transform: translateY(-50%) translateX(4px); color: #cbd5e1; font-size: 16px; opacity: 0; transition: opacity .15s, transform .15s; }
|
||
|
||
/* ══ MODAL ══ */
|
||
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(10,15,30,0.55); backdrop-filter: blur(6px); z-index: 1000; align-items: center; justify-content: center; padding: 20px; }
|
||
.modal-overlay.open { display: flex; animation: fadeIn .2s ease; }
|
||
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
|
||
.modal-box { background: #fff; border-radius: 20px; width: 100%; max-width: 780px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 32px 80px rgba(15,23,42,0.25); animation: slideUp .25s ease; }
|
||
@keyframes slideUp { from { transform: translateY(30px); opacity: 0 } to { transform: translateY(0); opacity: 1 } }
|
||
.modal-header { padding: 20px 24px 16px; border-bottom: 1px solid #e4eaf5; display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
|
||
.modal-ava { width: 50px; height: 50px; border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 21px; font-weight: 800; color: #fff; flex-shrink: 0; }
|
||
.modal-name { font-size: 17px; font-weight: 800; }
|
||
.modal-fy-badge { font-size: 11px; font-weight: 700; background: #eff6ff; color: #2563eb; padding: 3px 10px; border-radius: 99px; margin-top: 4px; display: inline-block; }
|
||
.modal-close { margin-left: auto; background: #f1f5f9; border: none; width: 34px; height: 34px; border-radius: 50%; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; color: #7c8db0; transition: background .2s; }
|
||
.modal-close:hover { background: #e2e8f0; color: #0f172a; }
|
||
.modal-summary { display: grid; grid-template-columns: repeat(4,1fr); background: #f8fafd; border-bottom: 1px solid #e4eaf5; flex-shrink: 0; }
|
||
.ms-box { padding: 14px 18px; text-align: center; border-right: 1px solid #e4eaf5; }
|
||
.ms-box:last-child { border-right: none; }
|
||
.ms-num { font-size: 19px; font-weight: 800; }
|
||
.ms-lbl { font-size: 10px; color: #7c8db0; font-weight: 700; letter-spacing: .05em; margin-top: 2px; }
|
||
.modal-body { padding: 22px 24px; overflow-y: auto; flex: 1; }
|
||
.target-edit-panel { display: grid; grid-template-columns: 1fr 170px 120px; gap: 12px; align-items: end; padding: 16px; margin-bottom: 18px; border: 1px solid #bfdbfe; border-radius: 14px; background: #eff6ff; }
|
||
.target-edit-title { font-size: 13px; font-weight: 800; color: #1e3a8a; margin-bottom: 4px; }
|
||
.target-edit-help { font-size: 11px; color: #64748b; font-weight: 600; }
|
||
.target-edit-input { width: 100%; height: 38px; border: 1px solid #93c5fd; border-radius: 9px; padding: 8px 12px; font-size: 14px; font-weight: 700; color: #0f172a; background: #fff; }
|
||
.target-edit-input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14); }
|
||
.target-edit-btn { height: 38px; border: none; border-radius: 9px; background: #2563eb; color: #fff; font-size: 13px; font-weight: 800; cursor: pointer; transition: background .2s, transform .2s; }
|
||
.target-edit-btn:hover { background: #1d4ed8; transform: translateY(-1px); }
|
||
.target-edit-btn:disabled { background: #94a3b8; cursor: not-allowed; transform: none; }
|
||
|
||
/* ── Modal Tabs ── */
|
||
.modal-tabs { display: flex; gap: 4px; background: #f1f5f9; border-radius: 10px; padding: 4px; margin-bottom: 20px; }
|
||
.modal-tab { flex: 1; padding: 9px 16px; border: none; border-radius: 8px; font-size: 13px; font-weight: 700; cursor: pointer; background: transparent; color: #7c8db0; transition: all .2s; }
|
||
.modal-tab.active { background: #fff; color: #0f172a; box-shadow: 0 2px 8px rgba(15,23,42,0.08); }
|
||
.modal-tab-panel { display: none; }
|
||
.modal-tab-panel.active { display: block; }
|
||
|
||
.overall-prog { background: linear-gradient(135deg,#0f172a,#1e293b); border-radius: 14px; padding: 18px 20px; color: #fff; margin-bottom: 20px; }
|
||
.op-label { font-size: 10px; font-weight: 700; letter-spacing: .08em; color: #94a3b8; }
|
||
.op-row { display: flex; justify-content: space-between; align-items: flex-end; margin: 6px 0 12px; }
|
||
.op-achieved { font-size: 24px; font-weight: 800; }
|
||
.op-target { font-size: 12px; color: #94a3b8; font-weight: 600; }
|
||
.op-pct { font-size: 26px; font-weight: 800; color: #818cf8; }
|
||
.op-bar-wrap { background: rgba(255,255,255,0.12); height: 7px; border-radius: 99px; overflow: hidden; }
|
||
.op-bar-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg,#818cf8,#67e8f9); }
|
||
.op-foot { display: flex; justify-content: space-between; font-size: 11px; color: #94a3b8; font-weight: 600; margin-top: 7px; }
|
||
.split-section-title { font-size: 13px; font-weight: 800; margin-bottom: 12px; }
|
||
.split-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.split-table thead th { background: #f8fafd; padding: 9px 12px; text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .07em; color: #7c8db0; font-weight: 500; border-bottom: 2px solid #e4eaf5; }
|
||
.split-table thead th:last-child { text-align: right; }
|
||
.split-table tbody tr { border-bottom: 1px solid #f1f5f9; }
|
||
.split-table tbody tr:hover { background: #f8fafd; }
|
||
.split-table td { padding: 12px; vertical-align: middle; }
|
||
.split-table td:last-child { text-align: right; }
|
||
.split-name { font-weight: 700; font-size: 13px; }
|
||
.split-dates { font-size: 11px; color: #7c8db0; font-weight: 600; margin-top: 1px; }
|
||
.mini-prog-wrap { width: 80px; height: 4px; background: #e4eaf5; border-radius: 99px; overflow: hidden; margin-top: 4px; }
|
||
.mini-prog-bar { height: 100%; border-radius: 99px; }
|
||
.split-pct-badge { display: inline-block; padding: 3px 10px; border-radius: 99px; font-size: 11px; font-weight: 700; }
|
||
.act-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
|
||
.act-chip { display: flex; align-items: center; gap: 6px; background: #f1f5f9; border-radius: 99px; padding: 5px 13px; font-size: 12px; font-weight: 700; }
|
||
.act-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||
|
||
/* Opportunities tab */
|
||
.opp-summary-cards { display: grid; grid-template-columns: repeat(3,1fr); gap: 12px; margin-bottom: 20px; }
|
||
.opp-card { background: #f8fafd; border-radius: 12px; padding: 16px; border: 1px solid #e4eaf5; text-align: center; }
|
||
.opp-card-val { font-size: 22px; font-weight: 800; color: #0f172a; }
|
||
.opp-card-lbl { font-size: 11px; color: #7c8db0; font-weight: 700; margin-top: 4px; letter-spacing: .04em; }
|
||
.opp-section-head { display: flex; justify-content: space-between; align-items: center; margin: 24px 0 12px; gap: 12px; flex-wrap: wrap; }
|
||
.opp-section-head .split-section-title { margin: 0; }
|
||
.opp-toggle-wrap { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
||
.opp-toggle-lbl { font-size: 12px; font-weight: 700; color: #94a3b8; transition: color .2s; }
|
||
.opp-toggle-lbl.active { color: #0f172a; }
|
||
.opp-toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }
|
||
.opp-toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||
.opp-toggle-slider { position: absolute; cursor: pointer; inset: 0; background: #cbd5e1; border-radius: 99px; transition: background .2s; }
|
||
.opp-toggle-slider::before { content: ""; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: transform .2s; box-shadow: 0 1px 3px rgba(15,23,42,0.15); }
|
||
.opp-toggle-switch input:checked + .opp-toggle-slider { background: #4f46e5; }
|
||
.opp-toggle-switch input:checked + .opp-toggle-slider::before { transform: translateX(20px); }
|
||
|
||
@media(max-width:900px) { .list-col-head,.achieve-row { grid-template-columns: 220px 1fr 110px 110px 110px; } .col-lbl:nth-child(6),.col-lbl:nth-child(7),.achieve-row>*:nth-child(6),.achieve-row>*:nth-child(7) { display: none; } }
|
||
@media(max-width:640px) { .list-col-head { display: none; } .achieve-row { grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px; } .achieve-row>*:not(:nth-child(1)):not(:nth-child(7)) { display: none; } .section-head { flex-direction: column; align-items: flex-start; gap: 12px; } .modal-summary { grid-template-columns: repeat(2,1fr); } .target-edit-panel { grid-template-columns: 1fr; } .opp-summary-cards { grid-template-columns: 1fr 1fr; } }
|
||
</style>
|
||
|
||
<div class="dash-container">
|
||
|
||
<!-- ── Dashboard Header ── -->
|
||
<div class="dash-header">
|
||
<div>
|
||
<div class="page-title">Branch Dashboard</div>
|
||
<div class="page-sub" id="fySubLabel">—</div>
|
||
</div>
|
||
<div class="fy-select-wrap">
|
||
<select id="financial_year" name="financial_year" onchange="onFinancialYearChange(this)">
|
||
<?php if(!empty($fin_years)): ?>
|
||
<?php foreach($fin_years as $year): ?>
|
||
<option value="<?= $year; ?>"><?= $year; ?></option>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Stat Cards ── -->
|
||
<div class="stat-cards">
|
||
<div class="card-hero stat-link" onclick="goSalesPage('leads', 'all')" title="View all leads">
|
||
<div class="stat-icon" style="background:#ede9fe;">👥</div>
|
||
<div class="stat-val"><?php echo $total_leads; ?></div>
|
||
<div class="stat-label">Total Leads</div>
|
||
</div>
|
||
<div class="card-hero stat-link" onclick="goSalesPage('activities', 'all')" title="View all activities">
|
||
<div class="stat-icon" style="background:#fce7f3;">⚡</div>
|
||
<div class="stat-val"><?php echo $total_acts ?></div>
|
||
<div class="stat-label">Total Activities</div>
|
||
</div>
|
||
<div class="card-hero stat-link" onclick="goSalesPage('activities', 'pending')" title="View pending activities">
|
||
<div class="stat-icon" style="background:#fef3c7;">⏳</div>
|
||
<div class="stat-val"><?php echo $total_pending_acts ?></div>
|
||
<div class="stat-label">Pending Activities</div>
|
||
</div>
|
||
<div class="card-hero stat-link" onclick="goSalesPage('activities', 'completed')" title="View completed activities">
|
||
<div class="stat-icon" style="background:#d1fae5;">✅</div>
|
||
<div class="stat-val"><?php echo $total_completed_acts ?></div>
|
||
<div class="stat-label">Completed Activities</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ TEAM ACHIEVEMENT SECTION ══ -->
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="section-title">Team Achievement Overview</div>
|
||
<!-- <div class="section-sub">Click any row to view split-wise & opportunities achievement</div> -->
|
||
<div class="section-sub"></div>
|
||
</div>
|
||
<div class="branch-summary">
|
||
<div style="font-size:11px;color:#7c8db0;font-weight:700;">Branch Total</div>
|
||
<div class="branch-amt" id="branchTotalAmt">—</div>
|
||
<div class="branch-bar-wrap"><div class="branch-bar-fill" id="branchBarFill" style="width:0%;"></div></div>
|
||
<div class="branch-pct-lbl" id="branchPctLbl">—</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="achieve-list">
|
||
<div class="list-col-head">
|
||
<div class="col-lbl">Member</div>
|
||
<div class="col-lbl">Progress</div>
|
||
<div class="col-lbl right">Achieved</div>
|
||
<div class="col-lbl right">Target</div>
|
||
<div class="col-lbl right">Remaining</div>
|
||
<div class="col-lbl center">Status</div>
|
||
<div class="col-lbl center">Acts</div>
|
||
</div>
|
||
<div id="achieveList"></div>
|
||
</div>
|
||
<br>
|
||
|
||
<!-- ── Main Grid ── -->
|
||
<div class="main-grid">
|
||
|
||
<!-- LEFT: Pending Activities -->
|
||
<div class="card">
|
||
<div class="table-header">
|
||
<h3 style="font-size:15px; font-weight:700; margin:0;">Pending Activities — All Team</h3>
|
||
<span style="color:#8896aa; font-size:12px; font-weight:600;">
|
||
<?= count($pending_acts) . ' ' . (count($pending_acts) == 1 ? 'activity' : 'activities') ?>
|
||
</span>
|
||
</div>
|
||
<div class="table-scroll-area">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Company</th>
|
||
<th>Activity & Assigned</th>
|
||
<th>Date</th>
|
||
<th>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (!empty($pending_acts)): ?>
|
||
<?php foreach ($pending_acts as $a): ?>
|
||
<?php
|
||
$activityIcons = ['Call'=>'📞','Email'=>'✉️','Meeting'=>'📅','Visit'=>'🚗','Demo'=>'🖥️','Share Docs'=>'📄','To Do'=>'✓'];
|
||
$icon = $activityIcons[$a['activity_type']] ?? '📌';
|
||
$statusClass = strtolower(str_replace(' ', '-', $a['status']));
|
||
$formattedscheduledDate = date('M d, Y, h:i A', strtotime($a['scheduled_date']));
|
||
?>
|
||
<tr>
|
||
<td><strong><?= esc($a['company_name']) ?></strong></td>
|
||
<td>
|
||
<div style="color:#ff6b35; font-weight:700; font-size:11px;"><?= $icon ?> <?= strtoupper(esc($a['activity_type'])) ?></div>
|
||
<div style="font-size:12px; color:#8896aa;"><?= esc($a['assigned_to_name'] ?? 'ID: ' . $a['assigned_to']) ?></div>
|
||
</td>
|
||
<td style="color:#4a5568; font-weight:500; font-size:13px;"><?= $formattedscheduledDate ?></td>
|
||
<td><span class="status-pill status-<?= $statusClass ?>"><?= ucfirst(esc($a['status'])) ?></span></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else: ?>
|
||
<tr><td colspan="4"><div class="empty-state"><div class="empty-icon">✓</div>No pending activities</div></td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT: Sales Team Performance + Activity Breakdown -->
|
||
<div class="right-panel">
|
||
<div style="font-size:15px; font-weight:700; padding-bottom:10px; border-bottom:1px solid #f0f4fa; flex-shrink:0;">
|
||
Sales Team Performance
|
||
</div>
|
||
<div class="team-members-scroll">
|
||
<?php
|
||
$visibleTeam = array_values(array_filter($team ?? [], function ($member) {
|
||
return !(stripos($member['role'] ?? '', 'admin') !== false
|
||
&& (int) ($member['total_acts'] ?? 0) === 0
|
||
&& (int) ($member['done_acts'] ?? 0) === 0);
|
||
}));
|
||
if (!empty($visibleTeam)):
|
||
$colors = ['#ff6b35','#667eea','#48bb78','#ed8936','#9f7aea'];
|
||
foreach ($visibleTeam as $member):
|
||
$firstLetter = strtoupper(substr($member['first_name'], 0, 1));
|
||
$fullName = $member['first_name'];
|
||
$color = $colors[abs(crc32($member['first_name'])) % count($colors)];
|
||
?>
|
||
<div class="team-member">
|
||
<div class="member-img" style="background:<?= $color ?>;"><?= $firstLetter ?></div>
|
||
<div style="flex:1;">
|
||
<div style="font-weight:700; font-size:14px;"><?= esc($fullName) ?></div>
|
||
<div style="font-size:11px; color:#8896aa;"><?= esc($member['role']) ?></div>
|
||
</div>
|
||
<div class="act-stats">
|
||
<div class="total"><?= $member['total_acts'] ?> acts</div>
|
||
<div class="done"><?= $member['done_acts'] ?> done</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; else: ?>
|
||
<div class="empty-state">No team data found</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="breakdown-section">
|
||
<h3 style="font-size:13px; font-weight:700; margin:0 0 12px 0; color:#1a202c;">Activity Breakdown</h3>
|
||
<?php
|
||
$activityConfig = [
|
||
'Call' => ['color'=>'#667eea','icon'=>'📞'],
|
||
'Email' => ['color'=>'#48bb78','icon'=>'✉️'],
|
||
'Meeting' => ['color'=>'#4299e1','icon'=>'📅'],
|
||
'Visit' => ['color'=>'#ecc94b','icon'=>'🚗'],
|
||
'Demo' => ['color'=>'#9f7aea','icon'=>'🖥️'],
|
||
'Share Docs' => ['color'=>'#ed8936','icon'=>'📄'],
|
||
'To Do' => ['color'=>'#718096','icon'=>'✓'],
|
||
];
|
||
?>
|
||
<?php if (!empty($activity_breakdown)): ?>
|
||
<?php foreach ($activity_breakdown as $item): ?>
|
||
<?php $cfg = $activityConfig[$item['activity_type']] ?? ['color'=>'#718096','icon'=>'📌']; ?>
|
||
<div class="breakdown-item">
|
||
<span style="min-width:90px;"><?= $cfg['icon'] ?> <?= esc($item['activity_type']) ?></span>
|
||
<div class="breakdown-bar-wrap">
|
||
<div class="breakdown-bar" style="width:<?= $item['percentage'] ?>%; background:<?= $cfg['color'] ?>;"></div>
|
||
</div>
|
||
<strong style="min-width:34px; text-align:right; color:#1a202c;"><?= $item['percentage'] ?>%</strong>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php else: ?>
|
||
<div style="text-align:center; color:#aaa; font-size:13px;">No activities found</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- end .main-grid -->
|
||
|
||
<!-- ── All Leads Overview ── -->
|
||
<div class="card" style="width:100%; box-sizing:border-box;">
|
||
<div class="table-header">
|
||
<h3 style="font-size:15px; font-weight:700; margin:0;">All Leads Overview</h3>
|
||
</div>
|
||
<div class="leads-table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Company</th>
|
||
<th>Status</th>
|
||
<th>Assigned To</th>
|
||
<th style="text-align:center;">Activities</th>
|
||
<th style="text-align:center;">Opportunities</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (!empty($leads_overview)): ?>
|
||
<?php foreach ($leads_overview as $lead): ?>
|
||
<?php $statusClass = strtolower(str_replace(' ', '-', $lead['status'])); ?>
|
||
<tr>
|
||
<td>
|
||
<strong>
|
||
<a href="javascript:void(0);"
|
||
onclick="goLeadDetail('<?= esc($lead['lead_id'] ?? '', 'attr') ?>')"
|
||
title="View lead details"
|
||
style="color:#0f172a; text-decoration:underline; text-underline-offset:2px;">
|
||
<?= esc($lead['company_name']) ?>
|
||
</a>
|
||
</strong>
|
||
</td>
|
||
<td><span class="status-pill status-<?= $statusClass ?>"><?= esc($lead['status']) ?></span></td>
|
||
<td><?= esc($lead['assigned_to'] ?? 'Unassigned') ?></td>
|
||
<td>
|
||
<div style="text-align:center; font-weight:700;">
|
||
<?php if ((int) $lead['activities'] > 0): ?>
|
||
<span class="activity-count-link"
|
||
title="View activities"
|
||
onclick="goLeadActivities('<?= esc($lead['assigned_to_id'] ?? '', 'attr') ?>')"><?= $lead['activities'] ?></span>
|
||
<?php else: ?>
|
||
<span class="activity-count-muted" title="No activities"><?= $lead['activities'] ?></span>
|
||
<?php endif; ?>
|
||
</div>
|
||
</td>
|
||
<td><div style="text-align:center; font-weight:700;"><?= $lead['opportunities'] ?></div></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else: ?>
|
||
<tr><td colspan="5"><div class="empty-state"><div class="empty-icon">👤</div>No leads found</div></td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- end .dash-container -->
|
||
|
||
<!-- ══ MODAL ══ -->
|
||
<div class="modal-overlay" id="modalOverlay">
|
||
<div class="modal-box">
|
||
<div class="modal-header">
|
||
<div class="modal-ava" id="modalAva"></div>
|
||
<div>
|
||
<div class="modal-name" id="modalName"></div>
|
||
<div class="modal-fy-badge" id="modalFyBadge"></div>
|
||
</div>
|
||
<button class="modal-close" onclick="closeModal()">✕</button>
|
||
</div>
|
||
<div class="modal-summary" id="modalSummary"></div>
|
||
<div class="modal-body" id="modalBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<script>
|
||
/* ── Data injected from PHP ── */
|
||
const TEAM = <?php echo json_encode($team_achievement ?? []); ?>;
|
||
const OPP_DATA = <?php echo json_encode($opp_achievement ?? []); ?>;
|
||
/* OPP_DATA shape per userId:
|
||
{ total_policies, total_exp_amt, target_amt,
|
||
policies: [{ policy_no, issue_date, amount, created_at }],
|
||
won_leads: [...], loss_leads: [...] } */
|
||
let currentModalOppUserId = null;
|
||
|
||
/* ── JS Helpers ── */
|
||
const ACT_ICONS = {Call:'📞',Email:'✉️',Meeting:'📅',Visit:'🚗',Demo:'🖥️','Share Docs':'📄','To Do':'✓'};
|
||
const ACT_COLORS = {Call:'#4f46e5',Email:'#10b981',Meeting:'#06b6d4',Visit:'#f59e0b',Demo:'#ec4899','Share Docs':'#f97316','To Do':'#64748b'};
|
||
const fmtIndianFull = value => '₹' + (Number(value) || 0).toLocaleString('en-IN', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2
|
||
});
|
||
const fmt = value => {
|
||
const amount = Number(value) || 0;
|
||
let display = '₹0';
|
||
if (Math.abs(amount) >= 100000) {
|
||
display = '₹' + (amount / 100000).toFixed(1) + 'L';
|
||
} else if (Math.abs(amount) >= 1000) {
|
||
display = '₹' + (amount / 1000).toFixed(1) + 'K';
|
||
} else if (amount !== 0) {
|
||
display = '₹' + amount.toLocaleString('en-IN', { maximumFractionDigits: 0 });
|
||
}
|
||
return '<span title="' + fmtIndianFull(amount) + '">' + display + '</span>';
|
||
};
|
||
const fmtFull = v => '₹' + Number(v).toLocaleString('en-IN');
|
||
const pct = (a, t) => t > 0 ? Math.min(100, Math.round(a / t * 100)) : 0;
|
||
const badge = p => p >= 100 ? ['b-achieved','🏆 Achieved'] : p >= 75 ? ['b-ontrack','🎯 On Track'] : p >= 50 ? ['b-behind','⚡ Behind'] : ['b-atrisk','⚠️ At Risk'];
|
||
const progColor= p => p >= 100 ? 'linear-gradient(90deg,#10b981,#34d399)' : p >= 75 ? 'linear-gradient(90deg,#06b6d4,#67e8f9)' : p >= 50 ? 'linear-gradient(90deg,#f97316,#fbbf24)' : 'linear-gradient(90deg,#ef4444,#fca5a5)';
|
||
const SALES_LEADS_URL = '<?= base_url('sales') ?>';
|
||
const SALES_ACTIVITIES_URL = '<?= base_url('sales/loadactivities') ?>';
|
||
const SALES_TARGETS_URL = '<?= base_url('sales/targets') ?>';
|
||
const OPPORTUNITY_DETAIL_BASE = '<?= base_url('/util/getLeadNonEB/') ?>';
|
||
|
||
function getSelectedDashboardFY() {
|
||
return document.getElementById('financial_year')?.value || getCurrentFY();
|
||
}
|
||
|
||
function redirectToOpportunity(leadTypeId, actualLeadId, opportunityId) {
|
||
if (!leadTypeId || !actualLeadId || !opportunityId) return;
|
||
|
||
window.location.href = OPPORTUNITY_DETAIL_BASE
|
||
+ encodeURIComponent(leadTypeId) + '/'
|
||
+ encodeURIComponent(actualLeadId) + '/'
|
||
+ encodeURIComponent(opportunityId);
|
||
}
|
||
|
||
function escOppCell(text) {
|
||
return String(text ?? '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function updateOppTableHeader(showWon) {
|
||
const headRow = document.getElementById('oppTableHead');
|
||
if (!headRow) return;
|
||
|
||
if (showWon) {
|
||
headRow.innerHTML =
|
||
'<th>Company</th>'
|
||
+ '<th>Opportunity Type</th>'
|
||
+ '<th>Date</th>';
|
||
} else {
|
||
headRow.innerHTML =
|
||
'<th>Company</th>'
|
||
+ '<th>Opportunity Type</th>'
|
||
+ '<th>Lost Reason</th>'
|
||
+ '<th>Date</th>';
|
||
}
|
||
}
|
||
|
||
function buildOpportunityTableRows(leads, showWon) {
|
||
return (leads || []).map(function(l) {
|
||
const opportunityType = [
|
||
l.lead_form_type || '',
|
||
].filter(Boolean).join('').toUpperCase();
|
||
const typeBadgeClass = l.lead_form_type === 'EB'
|
||
? 'style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"'
|
||
: 'style="background:#f0fdf4;color:#16a34a;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"';
|
||
const canRedirect = l.lead_form_type_id && l.actual_lead_id && l.opportunities_id;
|
||
const rowAttrs = canRedirect
|
||
? ' class="won-opportunity-row" title="Open opportunity" onclick=\'redirectToOpportunity('
|
||
+ JSON.stringify(String(l.lead_form_type_id)) + ','
|
||
+ JSON.stringify(String(l.actual_lead_id)) + ','
|
||
+ JSON.stringify(String(l.opportunities_id)) + ')\''
|
||
: '';
|
||
|
||
const lostReasonCell = showWon ? '' :
|
||
'<td style="font-size:12px;color:#64748b;max-width:220px;white-space:normal;">'
|
||
+ escOppCell((l.lost_reason || '').trim() || '—')
|
||
+ '</td>';
|
||
|
||
return '<tr' + rowAttrs + '>'
|
||
+ '<td style="font-weight:700;color:#0f172a;">' + escOppCell(l.company || '—') + '</td>'
|
||
+ '<td><span ' + typeBadgeClass + '>' + (opportunityType || '—') + '</span></td>'
|
||
+ lostReasonCell
|
||
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(l.created_at) + '</td>'
|
||
+ '</tr>';
|
||
}).join('');
|
||
}
|
||
|
||
function updateOppToggleLabels(showWon) {
|
||
const lostLbl = document.getElementById('oppToggleLblLost');
|
||
const wonLbl = document.getElementById('oppToggleLblWon');
|
||
if (lostLbl) lostLbl.classList.toggle('active', !showWon);
|
||
if (wonLbl) wonLbl.classList.toggle('active', showWon);
|
||
}
|
||
|
||
function renderOpportunitiesTable(userId, showWon) {
|
||
const opp = OPP_DATA[userId];
|
||
const tbody = document.getElementById('oppTableBody');
|
||
if (!tbody || !opp) return;
|
||
|
||
const leads = showWon ? (opp.won_leads || []) : (opp.loss_leads || []);
|
||
const colSpan = showWon ? 3 : 4;
|
||
updateOppTableHeader(showWon);
|
||
const rows = buildOpportunityTableRows(leads, showWon);
|
||
const emptyMsg = showWon ? 'No won opportunities found' : 'No lost opportunities found';
|
||
tbody.innerHTML = rows || '<tr><td colspan="' + colSpan + '" style="text-align:center;color:#aaa;padding:24px;">' + emptyMsg + '</td></tr>';
|
||
updateOppToggleLabels(showWon);
|
||
}
|
||
|
||
function onOppStatusToggle(checked) {
|
||
if (currentModalOppUserId) {
|
||
renderOpportunitiesTable(currentModalOppUserId, checked);
|
||
}
|
||
}
|
||
|
||
function goSalesPage(page, status, memberId) {
|
||
const url = new URL(page === 'activities' ? SALES_ACTIVITIES_URL : SALES_LEADS_URL, window.location.origin);
|
||
url.searchParams.set('fy', getSelectedDashboardFY());
|
||
url.searchParams.set('status', status || 'all');
|
||
if (memberId) {
|
||
url.searchParams.set('member', memberId);
|
||
}
|
||
window.location.href = url.toString();
|
||
}
|
||
|
||
function goLeadActivities(memberId) {
|
||
if (!memberId) return;
|
||
goSalesPage('activities', 'all', memberId);
|
||
}
|
||
|
||
function goLeadDetail(leadId) {
|
||
if (!leadId) return;
|
||
const url = new URL(SALES_LEADS_URL, window.location.origin);
|
||
url.searchParams.set('fy', getSelectedDashboardFY());
|
||
url.searchParams.set('status', 'all');
|
||
url.searchParams.set('lead_id', leadId);
|
||
window.location.href = url.toString();
|
||
}
|
||
|
||
function showToast(type, message) {
|
||
if (window.toastr && typeof toastr[type] === 'function') {
|
||
toastr[type](message);
|
||
return;
|
||
}
|
||
alert(message);
|
||
}
|
||
|
||
function refreshTargetInDashboard(userId, targetId, targetAmount) {
|
||
const member = TEAM.find(item => item.id == userId);
|
||
if (!member) return;
|
||
|
||
member.target_id = targetId || member.target_id;
|
||
member.target_amt = targetAmount;
|
||
if (Array.isArray(member.splits)) {
|
||
member.splits.forEach(split => {
|
||
split.target = Number((targetAmount / 4).toFixed(2));
|
||
});
|
||
}
|
||
|
||
if (OPP_DATA[userId]) {
|
||
OPP_DATA[userId].target_amt = targetAmount;
|
||
}
|
||
|
||
renderBranchSummary();
|
||
renderList();
|
||
}
|
||
|
||
async function getSavedTargetRecord(userId, fyYear, fallbackTarget, forceFetch = false) {
|
||
if (!forceFetch && fallbackTarget?.id && fallbackTarget?.target_amount) {
|
||
return fallbackTarget;
|
||
}
|
||
|
||
const res = await fetch(`${SALES_TARGETS_URL}/user/${userId}`);
|
||
if (!res.ok) {
|
||
return fallbackTarget || {};
|
||
}
|
||
|
||
const json = await res.json();
|
||
return (json.data || []).find(record => record.fy_year === fyYear) || fallbackTarget || {};
|
||
}
|
||
|
||
async function saveModalTarget(targetId, userId, fyYear) {
|
||
const input = document.getElementById('modalTargetAmount');
|
||
const button = document.getElementById('btnSaveModalTarget');
|
||
const amount = parseInt(input?.value || '0', 10);
|
||
|
||
if (!amount || amount <= 0) {
|
||
input?.focus();
|
||
showToast('warning', 'Please enter a valid target amount.');
|
||
return;
|
||
}
|
||
|
||
const originalText = button ? button.textContent : '';
|
||
if (button) {
|
||
button.disabled = true;
|
||
button.textContent = 'Saving...';
|
||
}
|
||
|
||
try {
|
||
const isUpdate = !!targetId;
|
||
const res = await fetch(isUpdate ? `${SALES_TARGETS_URL}/${targetId}` : SALES_TARGETS_URL, {
|
||
method: isUpdate ? 'PUT' : 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
user_id: userId,
|
||
fy_year: fyYear,
|
||
target_amount: amount
|
||
})
|
||
});
|
||
const result = await res.json();
|
||
|
||
if (!res.ok) {
|
||
throw new Error(result?.message || 'Unable to update target amount.');
|
||
}
|
||
|
||
const activeTabIndex = Array.from(document.querySelectorAll('.modal-tab'))
|
||
.findIndex(tab => tab.classList.contains('active'));
|
||
const updatedTarget = await getSavedTargetRecord(userId, fyYear, result?.data || {}, !isUpdate);
|
||
const updatedAmount = Number(updatedTarget.target_amount ?? amount);
|
||
|
||
refreshTargetInDashboard(userId, updatedTarget.id || targetId, updatedAmount);
|
||
openModal(userId);
|
||
if (activeTabIndex > 0) {
|
||
const tabButton = document.querySelectorAll('.modal-tab')[activeTabIndex];
|
||
if (tabButton) switchTab(activeTabIndex, tabButton);
|
||
}
|
||
|
||
showToast('success', result?.message || (isUpdate ? 'Target amount updated successfully.' : 'Target amount created successfully.'));
|
||
} catch (err) {
|
||
showToast('error', err.message || 'Unable to save target amount.');
|
||
if (button) {
|
||
button.disabled = false;
|
||
button.textContent = originalText || 'Save Target';
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ── Date formatters ── */
|
||
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||
|
||
function fmtIssueDate(str) {
|
||
// Input: "2026-03-10" or any date string → "Mar 10, 2026"
|
||
if (!str) return '—';
|
||
const d = new Date(str);
|
||
if (isNaN(d)) return str;
|
||
return MONTHS[d.getMonth()] + ' ' + String(d.getDate()).padStart(2,'0') + ', ' + d.getFullYear();
|
||
}
|
||
|
||
function fmtCreatedAt(str) {
|
||
// Input: "2025-03-04 10:00:00" → "Mar 04, 2025, 10:00 AM"
|
||
if (!str) return '—';
|
||
const d = new Date(str.replace(' ', 'T'));
|
||
if (isNaN(d)) return str;
|
||
let h = d.getHours(), mi = d.getMinutes();
|
||
const ampm = h >= 12 ? 'PM' : 'AM';
|
||
h = h % 12 || 12;
|
||
return MONTHS[d.getMonth()] + ' ' + String(d.getDate()).padStart(2,'0') + ', ' + d.getFullYear()
|
||
+ ', ' + String(h).padStart(2,'0') + ':' + String(mi).padStart(2,'0') + ' ' + ampm;
|
||
}
|
||
|
||
/* ── Financial Year helpers ── */
|
||
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 formatFYLabel(fy) {
|
||
if (!fy) return '';
|
||
const parts = fy.split('-');
|
||
if (parts.length === 2) return 'FY ' + parts[0] + '-' + parts[1].slice(-2);
|
||
return 'FY ' + fy;
|
||
}
|
||
|
||
/* ── Init ── */
|
||
function initFY() {
|
||
const select = document.getElementById('financial_year');
|
||
const params = new URLSearchParams(window.location.search);
|
||
let fy = params.get('fy');
|
||
if (!fy) {
|
||
fy = getCurrentFY();
|
||
let matched = false;
|
||
for (let opt of select.options) { if (opt.value === fy) { matched = true; break; } }
|
||
if (!matched && select.options.length > 0) fy = select.options[0].value;
|
||
}
|
||
select.value = fy;
|
||
updateFYLabel(fy);
|
||
renderBranchSummary();
|
||
renderList();
|
||
}
|
||
function updateFYLabel(fy) {
|
||
document.getElementById('fySubLabel').textContent = '' + formatFYLabel(fy);
|
||
}
|
||
function onFinancialYearChange(select) {
|
||
const url = new URL(window.location.href);
|
||
url.searchParams.set('fy', select.value);
|
||
window.location.href = url.toString();
|
||
}
|
||
|
||
/* ── Branch Summary ── */
|
||
function renderBranchSummary() {
|
||
if (!TEAM || TEAM.length === 0) return;
|
||
const totalAchieved = TEAM.reduce((s, m) => s + (parseFloat(m.achieved_amt) || 0), 0);
|
||
const totalTarget = TEAM.reduce((s, m) => s + (parseFloat(m.target_amt) || 0), 0);
|
||
const p = pct(totalAchieved, totalTarget);
|
||
document.getElementById('branchTotalAmt').innerHTML = fmt(totalAchieved) + ' <span style="font-size:13px;color:#7c8db0;font-weight:600;">/ ' + fmt(totalTarget) + '</span>';
|
||
document.getElementById('branchBarFill').style.width = p + '%';
|
||
document.getElementById('branchPctLbl').textContent = p + '% of branch target';
|
||
}
|
||
|
||
/* ── Achievement List ── */
|
||
function renderList() {
|
||
if (!TEAM || TEAM.length === 0) {
|
||
document.getElementById('achieveList').innerHTML = '<div class="empty-state"><div class="empty-icon">👤</div>No team achievement data</div>';
|
||
return;
|
||
}
|
||
const sorted = [...TEAM].sort((a, b) => pct(b.achieved_amt, b.target_amt) - pct(a.achieved_amt, a.target_amt));
|
||
document.getElementById('achieveList').innerHTML = sorted.map((m, i) => {
|
||
const p = pct(m.achieved_amt, m.target_amt);
|
||
const rem = Math.max(0, m.target_amt - m.achieved_amt);
|
||
const [bc, bt] = badge(p);
|
||
const grad = m.grad || 'linear-gradient(135deg,#4f46e5,#818cf8)';
|
||
const color = m.color || '#4f46e5';
|
||
return '<div class="achieve-row" onclick="openModal(' + m.id + ')">'
|
||
+ '<div class="member-cell">'
|
||
+ '<div style="font-size:14px;font-weight:800;color:#cbd5e1;min-width:26px;">#' + (i+1) + '</div>'
|
||
+ '<div class="m-avatar" style="background:' + grad + ';">' + (m.first_name||'?')[0] + '</div>'
|
||
+ '<div><div class="m-name">' + m.first_name + '</div></div>'
|
||
+ '</div>'
|
||
+ '<div style="padding-right:20px;">'
|
||
+ '<div class="prog-row"><span class="prog-pct" style="color:' + color + ';">' + p + '%</span><span class="prog-amts">' + fmt(m.achieved_amt) + ' / ' + fmt(m.target_amt) + '</span></div>'
|
||
+ '<div class="prog-bar-wrap"><div class="prog-bar" style="width:' + p + '%;background:' + grad + ';"></div></div>'
|
||
+ '</div>'
|
||
+ '<div class="num-cell"><div class="num-val">' + fmt(m.achieved_amt) + '</div></div>'
|
||
+ '<div class="num-cell"><div class="num-val" style="color:#7c8db0;">' + fmt(m.target_amt) + '</div></div>'
|
||
+ '<div class="num-cell"><div class="num-val" style="color:' + (rem > 0 ? '#f97316' : '#10b981') + ';">' + (rem > 0 ? fmt(rem) : '—') + '</div></div>'
|
||
+ '<div style="display:flex;justify-content:center;"><span class="badge ' + bc + '">' + bt + '</span></div>'
|
||
+ '<div class="num-cell center-col"><div class="num-val">' + m.done_acts + '<span style="color:#cbd5e1;font-size:12px;font-weight:600;">/' + m.total_acts + '</span></div><div class="num-lbl">DONE/TOTAL</div></div>'
|
||
+ '<span class="row-arrow">›</span>'
|
||
+ '</div>';
|
||
}).join('');
|
||
}
|
||
|
||
/* ── Modal ── */
|
||
function openModal(id) {
|
||
const m = TEAM.find(x => x.id == id);
|
||
if (!m) return;
|
||
|
||
const p = pct(m.achieved_amt, m.target_amt);
|
||
const rem = Math.max(0, m.target_amt - m.achieved_amt);
|
||
const [bc, bt] = badge(p);
|
||
const grad = m.grad || 'linear-gradient(135deg,#4f46e5,#818cf8)';
|
||
|
||
const fySelect = document.getElementById('financial_year');
|
||
const currentFY = fySelect ? fySelect.value : getCurrentFY();
|
||
const fyLabel = formatFYLabel(currentFY);
|
||
|
||
document.getElementById('modalAva').style.background = grad;
|
||
document.getElementById('modalAva').textContent = (m.first_name || '?')[0];
|
||
document.getElementById('modalName').textContent = m.first_name;
|
||
document.getElementById('modalFyBadge').textContent = fyLabel;
|
||
|
||
document.getElementById('modalSummary').innerHTML =
|
||
'<div class="ms-box"><div class="ms-num" style="color:#4f46e5;">' + fmt(m.achieved_amt) + '</div><div class="ms-lbl">ACHIEVED</div></div>'
|
||
+ '<div class="ms-box"><div class="ms-num">' + fmt(m.target_amt) + '</div><div class="ms-lbl">TARGET</div></div>'
|
||
+ '<div class="ms-box"><div class="ms-num" style="color:#f97316;">' + fmt(rem) + '</div><div class="ms-lbl">REMAINING</div></div>'
|
||
+ '<div class="ms-box"><div class="ms-num" style="color:' + (p>=75?'#10b981':p>=50?'#f97316':'#ef4444') + ';">' + p + '%</div><div class="ms-lbl">ACHIEVED %</div></div>';
|
||
|
||
const hasTargetRecord = !!m.target_id;
|
||
const targetEditorHtml =
|
||
'<div class="target-edit-panel">'
|
||
+ '<div>'
|
||
+ '<div class="target-edit-title">' + (hasTargetRecord ? 'Edit Target Amount' : 'Create Target Amount') + '</div>'
|
||
+ '<div class="target-edit-help">' + (hasTargetRecord ? 'Update' : 'Create') + ' target for ' + m.first_name + ' - ' + currentFY + '</div>'
|
||
+ '</div>'
|
||
+ '<div>'
|
||
+ '<input type="text" id="modalTargetAmount" class="target-edit-input" value="' + Math.round(m.target_amt || 0) + '" placeholder="Target amount" oninput="this.value=this.value.replace(/[^0-9]/g, \'\')">'
|
||
+ '</div>'
|
||
+ '<button type="button" class="target-edit-btn" id="btnSaveModalTarget" onclick="saveModalTarget(' + (m.target_id || 0) + ', ' + m.id + ', \'' + currentFY + '\')" title="' + (hasTargetRecord ? 'Update target amount' : 'Create target amount') + '">' + (hasTargetRecord ? 'Update Target' : 'Create Target') + '</button>'
|
||
+ '</div>';
|
||
|
||
/* ── Overall progress block (shared) ── */
|
||
const overallHtml =
|
||
'<div class="overall-prog">'
|
||
+ '<div class="op-label">OVERALL TARGET ACHIEVEMENT</div>'
|
||
+ '<div class="op-row">'
|
||
+ '<div><div class="op-achieved">' + fmt(m.achieved_amt) + '</div><div class="op-target">of ' + fmt(m.target_amt) + ' target</div></div>'
|
||
+ '<div class="op-pct">' + p + '%</div>'
|
||
+ '</div>'
|
||
+ '<div class="op-bar-wrap"><div class="op-bar-fill" style="width:' + p + '%;"></div></div>'
|
||
+ '<div class="op-foot"><span>Remaining: <strong style="color:#fbbf24;">' + fmt(rem) + '</strong></span>'
|
||
+ '<span class="badge ' + bc + '" style="background:rgba(255,255,255,.1);color:#e2e8f0;">' + bt + '</span>'
|
||
+ '</div>'
|
||
+ '</div>';
|
||
|
||
/* ════════════════════════════
|
||
TAB 1 — Quarter-wise splits
|
||
════════════════════════════ */
|
||
const splits = m.splits || [];
|
||
const splitRows = splits.map(function(s) {
|
||
const sp = pct(s.achieved, s.target);
|
||
const srem = Math.max(0, s.target - s.achieved);
|
||
const [sbc, sbt] = badge(sp);
|
||
|
||
/* % cell: number on top, badge below (badge hidden when 0%) */
|
||
const pctColor = sp >= 75 ? '#10b981' : sp >= 50 ? '#f97316' : '#ef4444';
|
||
const pctCell = '<div style="font-weight:800;font-size:15px;color:' + pctColor + ';">' + sp + '%</div>'
|
||
+ (sp > 0 ? '<div style="margin-top:3px;"><span class="split-pct-badge ' + sbc + '" style="font-size:10px;padding:2px 8px;">' + sbt + '</span></div>' : '');
|
||
|
||
/* Activities cell: total bold on top, done in muted smaller text below */
|
||
const actsCell = '<div style="font-weight:800;font-size:15px;">' + s.acts + '</div>'
|
||
+ '<div style="font-size:11px;color:#94a3b8;font-weight:600;margin-top:2px;">' + s.done + ' done</div>';
|
||
|
||
return '<tr>'
|
||
+ '<td><div class="split-name">' + s.name + '</div><div class="split-dates">📅 ' + s.start + ' → ' + s.end + '</div></td>'
|
||
+ '<td><div style="font-weight:800;">' + fmt(s.achieved) + '</div><div class="mini-prog-wrap"><div class="mini-prog-bar" style="width:' + sp + '%;background:' + progColor(sp) + ';"></div></div></td>'
|
||
+ '<td style="font-weight:700;color:#7c8db0;">' + fmt(s.target) + '</td>'
|
||
+ '<td style="font-weight:700;color:#f97316;">' + (srem > 0 ? fmt(srem) : '—') + '</td>'
|
||
+ '<td>' + pctCell + '</td>'
|
||
+ '<td>' + actsCell + '</td>'
|
||
+ '<td style="font-weight:700;">' + s.leads + '</td>'
|
||
+ '</tr>';
|
||
}).join('');
|
||
|
||
const actChips = Object.entries(m.activities || {}).map(function(entry) {
|
||
const k = entry[0], v = entry[1];
|
||
return '<div class="act-chip">'
|
||
+ '<div class="act-dot" style="background:' + (ACT_COLORS[k]||'#64748b') + ';"></div>'
|
||
+ (ACT_ICONS[k]||'📌') + ' ' + k + ': <strong>' + v + '</strong>'
|
||
+ '</div>';
|
||
}).join('');
|
||
|
||
const tab1Html = overallHtml
|
||
+ '<div class="split-section-title">📊 Quarter-wise Achievement Breakdown</div>'
|
||
+ '<div style="overflow-x:auto;margin-bottom:20px;">'
|
||
+ '<table class="split-table">'
|
||
+ '<thead><tr>'
|
||
+ '<th>Quarter</th><th>Achieved</th><th>Target</th><th>Remaining</th>'
|
||
+ '<th style="text-align:center;">Achievement %</th>'
|
||
+ '<th style="text-align:center;">Activities</th>'
|
||
+ '<th style="text-align:center;">Leads</th>'
|
||
+ '</tr></thead>'
|
||
+ '<tbody>' + (splitRows || '<tr><td colspan="7" style="text-align:center;color:#aaa;padding:20px;">No split data available</td></tr>') + '</tbody>'
|
||
+ '</table>'
|
||
+ '</div>'
|
||
+ '<div class="split-section-title">⚡ Activity Breakdown</div>'
|
||
+ '<div class="act-chips">' + (actChips || '<span style="color:#aaa;font-size:13px;">No activity data</span>') + '</div>';
|
||
|
||
/* ════════════════════════════
|
||
TAB 2 — Opportunities
|
||
════════════════════════════ */
|
||
const opp = OPP_DATA[id] || null;
|
||
let tab2Html = '';
|
||
|
||
if (opp) {
|
||
const oppPct = pct(opp.total_exp_amt, opp.target_amt);
|
||
const [obc, obt] = badge(oppPct);
|
||
const oppPctColor = oppPct >= 75 ? '#10b981' : oppPct >= 50 ? '#f97316' : '#ef4444';
|
||
|
||
/* Summary cards */
|
||
const summaryCards =
|
||
'<div class="opp-summary-cards">'
|
||
+ '<div class="opp-card"><div class="opp-card-val" style="color:#4f46e5;">' + opp.total_policies + '</div><div class="opp-card-lbl">TOTAL POLICIES WON</div></div>'
|
||
+ '<div class="opp-card"><div class="opp-card-val" style="color:#10b981;">' + fmtFull(opp.total_exp_amt) + '</div><div class="opp-card-lbl">TOTAL EXP AMOUNT</div></div>'
|
||
+ '<div class="opp-card"><div class="opp-card-val" style="color:' + oppPctColor + ';">' + oppPct + '%</div><div class="opp-card-lbl">VS TARGET</div></div>'
|
||
+ '</div>';
|
||
|
||
/* Dark progress bar */
|
||
const oppProgress =
|
||
'<div style="background:linear-gradient(135deg,#0f172a,#1e293b);border-radius:14px;padding:18px 20px;color:#fff;margin-bottom:20px;">'
|
||
+ '<div class="op-label">OPPORTUNITIES ACHIEVEMENT</div>'
|
||
+ '<div class="op-row">'
|
||
+ '<div><div class="op-achieved">' + fmtFull(opp.total_exp_amt) + '</div><div class="op-target">of ' + fmtFull(opp.target_amt) + ' target</div></div>'
|
||
+ '<div class="op-pct">' + oppPct + '%</div>'
|
||
+ '</div>'
|
||
+ '<div class="op-bar-wrap"><div class="op-bar-fill" style="width:' + oppPct + '%;"></div></div>'
|
||
+ '<div class="op-foot"><span>' + opp.total_policies + ' policies won</span>'
|
||
+ '<span class="badge ' + obc + '" style="background:rgba(255,255,255,.1);color:#e2e8f0;">' + obt + '</span>'
|
||
+ '</div>'
|
||
+ '</div>';
|
||
|
||
/* Policy rows — NO quarter grouping */
|
||
const policies = opp.policies || [];
|
||
const policyRows = policies.map(function(pol) {
|
||
return '<tr>'
|
||
+ '<td style="font-weight:700;color:#0f172a;">' + (pol.policy_no || '—') + '</td>'
|
||
+ '<td style="font-weight:600;color:#334155;">' + fmtIssueDate(pol.issue_date) + '</td>'
|
||
+ '<td style="font-weight:800;color:#0f172a;">' + fmtFull(pol.amount || 0) + '</td>'
|
||
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(pol.created_at) + '</td>'
|
||
+ '</tr>';
|
||
}).join('');
|
||
|
||
tab2Html = "";
|
||
// tab2Html = summaryCards
|
||
// + oppProgress
|
||
// + '<div class="split-section-title">📋 Policy Details</div>'
|
||
// + '<div style="overflow-x:auto;">'
|
||
// + '<table class="split-table">'
|
||
// + '<thead><tr>'
|
||
// + '<th>Policy No.</th>'
|
||
// + '<th>Issue Date</th>'
|
||
// + '<th>Amount</th>'
|
||
// + '<th>Created Date</th>'
|
||
// + '</tr></thead>'
|
||
// + '<tbody>' + (policyRows || '<tr><td colspan="4" style="text-align:center;color:#aaa;padding:24px;">No policies found</td></tr>') + '</tbody>'
|
||
// + '</table>'
|
||
// + '</div>';
|
||
|
||
currentModalOppUserId = id;
|
||
|
||
tab2Html +=
|
||
'<div class="opp-section-head">'
|
||
+ '<div class="split-section-title">Opportunities</div>'
|
||
+ '<div class="opp-toggle-wrap">'
|
||
+ '<span class="opp-toggle-lbl" id="oppToggleLblLost">Lost</span>'
|
||
+ '<label class="opp-toggle-switch" title="Toggle between won and lost opportunities">'
|
||
+ '<input type="checkbox" id="oppStatusToggle" checked onchange="onOppStatusToggle(this.checked)">'
|
||
+ '<span class="opp-toggle-slider"></span>'
|
||
+ '</label>'
|
||
+ '<span class="opp-toggle-lbl active" id="oppToggleLblWon">Won</span>'
|
||
+ '</div>'
|
||
+ '</div>'
|
||
+ '<div style="overflow-x:auto;">'
|
||
+ '<table class="split-table">'
|
||
+ '<thead><tr id="oppTableHead">'
|
||
+ '<th>Company</th>'
|
||
+ '<th>Opportunity Type</th>'
|
||
+ '<th>Date</th>'
|
||
+ '</tr></thead>'
|
||
+ '<tbody id="oppTableBody"></tbody>'
|
||
+ '</table>'
|
||
+ '</div>';
|
||
|
||
} else {
|
||
currentModalOppUserId = null;
|
||
tab2Html = '<div class="empty-state"><div class="empty-icon">📋</div>No opportunities data available</div>';
|
||
}
|
||
|
||
/* ── Inject tabs ── */
|
||
document.getElementById('modalBody').innerHTML =
|
||
targetEditorHtml
|
||
+ '<div class="modal-tabs">'
|
||
+ '<button class="modal-tab active" onclick="switchTab(0,this)">📊 ' + fyLabel + ' Split-wise Achievement</button>'
|
||
+ '<button class="modal-tab" onclick="switchTab(1,this)">🏆 Opportunities Achievement</button>'
|
||
+ '</div>'
|
||
+ '<div class="modal-tab-panel active" id="tabPanel0">' + tab1Html + '</div>'
|
||
+ '<div class="modal-tab-panel" id="tabPanel1">' + tab2Html + '</div>';
|
||
|
||
document.getElementById('modalOverlay').classList.add('open');
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
if (currentModalOppUserId && OPP_DATA[currentModalOppUserId]) {
|
||
const toggle = document.getElementById('oppStatusToggle');
|
||
if (toggle) toggle.checked = true;
|
||
renderOpportunitiesTable(currentModalOppUserId, true);
|
||
} else {
|
||
currentModalOppUserId = null;
|
||
}
|
||
}
|
||
|
||
function switchTab(idx, btn) {
|
||
document.querySelectorAll('.modal-tab').forEach(function(t) { t.classList.remove('active'); });
|
||
document.querySelectorAll('.modal-tab-panel').forEach(function(p) { p.classList.remove('active'); });
|
||
btn.classList.add('active');
|
||
document.getElementById('tabPanel' + idx).classList.add('active');
|
||
}
|
||
function closeModal() {
|
||
document.getElementById('modalOverlay').classList.remove('open');
|
||
document.body.style.overflow = '';
|
||
currentModalOppUserId = null;
|
||
}
|
||
|
||
document.getElementById('modalOverlay').addEventListener('click', function(e) { if (e.target === e.currentTarget) closeModal(); });
|
||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); });
|
||
|
||
document.addEventListener('DOMContentLoaded', initFY);
|
||
</script>
|