{"id":146,"date":"2026-03-31T07:01:05","date_gmt":"2026-03-31T07:01:05","guid":{"rendered":"https:\/\/ph-portal.zyneventures.com\/?page_id=146"},"modified":"2026-03-31T07:04:55","modified_gmt":"2026-03-31T07:04:55","slug":"productivity","status":"publish","type":"page","link":"https:\/\/ph-portal.zyneventures.com\/index.php\/productivity\/","title":{"rendered":"Productivity"},"content":{"rendered":"<style>.kadence-column146_2a1be6-2b > .kt-inside-inner-col,.kadence-column146_2a1be6-2b > .kt-inside-inner-col:before{border-top-left-radius:0px;border-top-right-radius:0px;border-bottom-right-radius:0px;border-bottom-left-radius:0px;}.kadence-column146_2a1be6-2b > .kt-inside-inner-col{column-gap:var(--global-kb-gap-sm, 1rem);}.kadence-column146_2a1be6-2b > .kt-inside-inner-col{flex-direction:column;}.kadence-column146_2a1be6-2b > .kt-inside-inner-col > .aligncenter{width:100%;}.kadence-column146_2a1be6-2b > .kt-inside-inner-col:before{opacity:0.3;}.kadence-column146_2a1be6-2b{position:relative;}@media all and (max-width: 1024px){.kadence-column146_2a1be6-2b > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}@media all and (max-width: 767px){.kadence-column146_2a1be6-2b > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}<\/style>\n<div class=\"wp-block-kadence-column kadence-column146_2a1be6-2b\"><div class=\"kt-inside-inner-col\"><style>.kb-row-layout-id146_4f5a2a-fa > .kt-row-column-wrap{align-content:start;}:where(.kb-row-layout-id146_4f5a2a-fa > .kt-row-column-wrap) > .wp-block-kadence-column{justify-content:start;}.kb-row-layout-id146_4f5a2a-fa > .kt-row-column-wrap{column-gap:var(--global-kb-gap-md, 2rem);row-gap:var(--global-kb-gap-md, 2rem);padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;grid-template-columns:minmax(0, 1fr);}.kb-row-layout-id146_4f5a2a-fa > .kt-row-layout-overlay{opacity:0.30;}@media all and (max-width: 1024px){.kb-row-layout-id146_4f5a2a-fa > .kt-row-column-wrap{grid-template-columns:minmax(0, 1fr);}}@media all and (max-width: 767px){.kb-row-layout-id146_4f5a2a-fa > .kt-row-column-wrap{grid-template-columns:minmax(0, 1fr);}}<\/style><div class=\"kb-row-layout-wrap kb-row-layout-id146_4f5a2a-fa alignnone wp-block-kadence-rowlayout\"><div class=\"kt-row-column-wrap kt-has-1-columns kt-row-layout-equal kt-tab-layout-inherit kt-mobile-layout-row kt-row-valign-top\">\n<style>.kadence-column146_630938-05 > .kt-inside-inner-col,.kadence-column146_630938-05 > .kt-inside-inner-col:before{border-top-left-radius:0px;border-top-right-radius:0px;border-bottom-right-radius:0px;border-bottom-left-radius:0px;}.kadence-column146_630938-05 > .kt-inside-inner-col{column-gap:var(--global-kb-gap-sm, 1rem);}.kadence-column146_630938-05 > .kt-inside-inner-col{flex-direction:column;}.kadence-column146_630938-05 > .kt-inside-inner-col > .aligncenter{width:100%;}.kadence-column146_630938-05 > .kt-inside-inner-col:before{opacity:0.3;}.kadence-column146_630938-05{position:relative;}@media all and (max-width: 1024px){.kadence-column146_630938-05 > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}@media all and (max-width: 767px){.kadence-column146_630938-05 > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}<\/style>\n<div class=\"wp-block-kadence-column kadence-column146_630938-05\"><div class=\"kt-inside-inner-col\">\n<style>\n    :root {\n      --primary-blue: #0180FF;\n      --hover-blue:   #3499FF;\n      --text-dark:    #212529;\n      --text-muted:   #8D8C9C;\n      --border-color: #EFEFEF;\n      --bg-light:     #F8F9FA;\n      --error-red:    #D93025;\n      --success-green:#1E8E3E;\n    }\n    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n    body { \n        font-family: 'Sora', sans-serif; \n        color: var(--text-dark); \n        background: #fff; \n        min-height: 100vh; \n    }\n\n    \/* PAGE WRAPPER *\/\n    .page-wrapper { \n        \/* padding: 36px 40px 60px;  *\/\n        max-width: 100%; \n    }\n\n    \/* PAGE TITLE *\/\n    .page-title { \n        font-size: 21px; \n        font-weight: 500; \n        text-transform: uppercase; \n        letter-spacing: 0.5px; \n        margin-bottom: 20px; \n    }\n\n    \/* FILTER ROW *\/\n    .filter-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }\n\n    \/* label + select group *\/\n    .sel-group { \n        display: flex; \n        flex-direction: column; \n        gap: 5px; flex: 1 1 150px; \n        min-width: 140px; \n        max-width: 200px; \n    }\n    .sel-group label { font-size: 12px; font-weight: 400; color: var(--text-dark); }\n    .sel-group label .req { color: var(--error-red); }\n\n    \/* custom select *\/\n    .sel-wrap { position: relative; width: 100%; }\n    .sel-wrap select {\n      width: 100%; \n      appearance: none; \n      -webkit-appearance: none;\n      padding: 9px 32px 9px 14px; \n      border: 1px solid #DDE2E8; \n      border-radius: 7px;\n      font-family: 'Sora', sans-serif; \n      font-size: 12px; \n      color: var(--text-dark);\n      background: #fff; \n      cursor: pointer; \n      outline: none; \n      transition: border-color .2s;\n    }\n    .sel-wrap select:focus { border-color: var(--primary-blue); }\n    .sel-wrap::after {\n      content: ''; \n      position: absolute; \n      right: 12px; \n      top: 50%; \n      transform: translateY(-50%);\n      width: 0; height: 0;\n      border-left: 5px solid transparent; \n      border-right: 5px solid transparent;\n      border-top: 5px solid #8D8C9C; \n      pointer-events: none;\n    }\n\n    \/* buttons *\/\n    .btn {\n      padding: 9px 22px; \n      border-radius: 7px; \n      font-family: 'Sora', sans-serif;\n      font-size: 12px; \n      font-weight: 400; \n      cursor: pointer; \n      border: none;\n      transition: background .2s; \n      white-space: nowrap;\n    }\n    .btn-primary { background: var(--primary-blue); color: #fff; }\n    .btn-primary:hover { background: var(--hover-blue); }\n    .btn-primary:disabled { background: #a0c4ff; cursor: not-allowed; }\n    .btn-csv { background: var(--primary-blue); color: #fff; }\n    .btn-csv:hover { background: var(--hover-blue); }\n\n    .btn-align { display: flex; align-items: flex-end; gap: 10px; }\n    .csv-row { margin-top: 12px; }\n\n    \/* REVERSE SIGN OFF BANNER *\/\n    .rsof-banner {\n      background: #FFC107; color: #212529;\n      font-size: 12px; \n      font-weight: 400;\n      padding: 8px 18px; \n      border-radius: 6px;\n      margin-bottom: 16px; \n      display: inline-block;\n    }\n\n    \/* SECTION TITLE *\/\n    .section-title { \n        font-size: 21px; \n        font-weight: 500; \n        color: var(--text-dark); \n        margin-bottom: 14px; \n    }\n\n    \/* STAT CARDS *\/\n    .stat-cards { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 28px; }\n    .stat-card {\n      background: #fff; \n      border: 1px solid var(--border-color);\n      border-radius: 10px; \n      padding: 14px 18px; \n      min-width: 115px; \n      flex: 1 1 115px;\n    }\n    .stat-card .s-label { \n      font-size: 12px; \n      color: var(--text-muted); \n      margin-bottom: 6px; \n      white-space: nowrap; \n    }\n    .stat-card .s-value { font-size: 21px; font-weight: 500; color: var(--text-dark); }\n    .stat-card .s-value.red   { color: var(--error-red); }\n    .stat-card .s-value.green { color: var(--success-green); }\n\n    \/* results section hidden until search *\/\n    .results-section { display: none; }\n    .results-section.visible { display: block; }\n\n    \/* TABS \u2014 exact match quality_summary.html *\/\n    .tab-bar {\n      padding-top: 15px; \n      display: flex;\n      border-bottom: 2px solid var(--border-color); \n      margin-bottom: 0;\n    }\n    .tab-item {\n      padding: 12px 22px; \n      font-size: 16px; \n      font-weight: 400; \n      color: var(--text-muted);\n      cursor: pointer; \n      position: relative; \n      white-space: nowrap; \n      user-select: none;\n    }\n    .tab-item.active { color: #212529; font-weight: 500; }\n    .tab-item.active::after {\n      content: ''; \n      position: absolute; \n      bottom: -3px; \n      left: 0; \n      right: 0;\n      height: 3px; \n      background: var(--primary-blue);\n    }\n    .tab-item:hover:not(.active) { color: var(--text-muted); }\n\n    \/* TABLE WRAPPER *\/\n    .table-wrapper {\n      overflow-x: auto; \n      margin-top: 0;\n      border: 1px solid var(--border-color); \n      border-top: none; \n      border-radius: 0 0 10px 10px;\n    }\n    table { width: 100%; border-collapse: collapse; font-size: 12px; min-width: 980px; }\n    thead tr { background: #f5f5f5; }\n    thead th {\n      padding: 8px 15px; \n      font-weight: 500; \n      font-size: 12px; \n      text-transform: uppercase;\n      letter-spacing: .3px; \n      color: #212529; \n      border-bottom: 1px solid var(--border-color);\n      white-space: nowrap; \n      text-align: left;\n    }\n    tbody tr { border-bottom: 1px solid var(--border-color); transition: background .15s; }\n    tbody tr:last-child { border-bottom: none; }\n    tbody tr:hover { background: #f5f5f5; }\n    tbody td { padding: 10px 15px; color: var(--text-dark); vertical-align: middle; white-space: nowrap; }\n\n    \/* attendance badges *\/\n    .badge-present {\n      background: #E6F4EA; \n      color: var(--success-green);\n      border-radius: 20px; \n      padding: 3px 10px;\n      font-size: 12px; \n      font-weight: 400; \n      display: inline-flex; \n      align-items: center; \n      gap: 4px;\n    }\n    .badge-absent {\n      background: #FCE8E6; \n      color: var(--error-red);\n      border-radius: 20px; \n      padding: 3px 10px;\n      font-size: 12px; \n      font-weight: 400; \n      display: inline-flex; \n      align-items: center; \n      gap: 4px;\n    }\n    .badge-present::before { content: '\u25cf'; font-size: 7px; }\n    .badge-absent::before  { content: '\u25cf'; font-size: 7px; }\n\n    \/* state rows *\/\n    .state-row td { \n        text-align: center; \n        padding: 60px; \n        color: var(--text-muted); \n        font-size: 20px; \n        font-weight: 400; \n    }\n    .spinner {\n      display: inline-block; \n      width: 18px; \n      height: 18px;\n      border: 2px solid #ddd; \n      border-top-color: var(--primary-blue);\n      border-radius: 50%; \n      animation: spin .7s linear infinite;\n      vertical-align: middle; \n      margin-right: 6px;\n    }\n    @keyframes spin { to { transform: rotate(360deg); } }\n\n    \/* PAGINATION *\/\n    .pagination-footer {\n      display: flex; \n      justify-content: space-between; \n      align-items: center;\n      padding: 16px 4px; \n      font-size: 12px; \n      color: var(--text-muted); \n      flex-wrap: wrap; \n      gap: 10px;\n    }\n    .pagination-controls { display: flex; align-items: center; gap: 6px; }\n    .nav-btn {\n      padding: 6px 14px; \n      border-radius: 6px; \n      cursor: pointer; \n      font-size: 12px;\n      background: transparent; \n      color: #5f6368; \n      user-select: none; \n      transition: background .2s;\n      border: 1px solid var(--border-color);\n    }\n    .nav-btn:hover:not(.disabled) { background: transparent; }\n    .nav-btn.disabled { opacity: .4; pointer-events: none; }\n    .page-num {\n      width: 32px; \n      height: 32px; \n      border-radius: 50%;\n      display: flex; \n      align-items: center; \n      justify-content: center;\n      cursor: pointer; \n      font-size: 12px; \n      background: transparent; \n      color: #5f6368;\n      border: 1px solid var(--border-color); \n      transition: background .2s;\n    }\n    .page-num.active { background: var(--primary-blue); color: #fff; border-color: var(--primary-blue); }\n    .page-num:hover:not(.active) { background: transparent; }\n\n    \/* RESPONSIVE *\/\n    @media (max-width: 768px) {\n      body { padding-left: 20px; }\n    }\n    @media (max-width: 900px) {\n      .page-wrapper { padding: 15px 15px 30px; }\n      .sel-group { max-width: 100%; }\n      .tab-item { padding: 10px 14px; font-size: 12px; }\n      .stat-card .s-value { font-size: 18px; }\n    }\n    @media (max-width: 600px) {\n      .tab-bar { overflow-x: auto; }\n      .filter-row { gap: 8px; }\n      .sel-group { flex: 1 1 100%; max-width: 100%; }\n      .stat-cards { gap: 8px; }\n      .stat-card { flex: 1 1 100px; min-width: 100px; }\n    }\n  <\/style>\n\n\n<div class=\"page-wrapper\">\n\n  <!-- TITLE -->\n  <div class=\"page-title\">PRODUCTIVITY LIST<\/div>\n\n  <!-- FILTER FORM -->\n  <div class=\"filter-row\">\n    <div class=\"sel-group\">\n      <label>Month<span class=\"req\">*<\/span><\/label>\n      <div class=\"sel-wrap\">\n        <select id=\"selMonth\">\n          <option value=\"\">Select Month<\/option>\n          <option value=\"1\">January<\/option><option value=\"2\">February<\/option>\n          <option value=\"3\">March<\/option><option value=\"4\">April<\/option>\n          <option value=\"5\">May<\/option><option value=\"6\">June<\/option>\n          <option value=\"7\">July<\/option><option value=\"8\">August<\/option>\n          <option value=\"9\">September<\/option><option value=\"10\">October<\/option>\n          <option value=\"11\">November<\/option><option value=\"12\">December<\/option>\n        <\/select>\n      <\/div>\n    <\/div>\n\n    <div class=\"sel-group\">\n      <label>Year<span class=\"req\">*<\/span><\/label>\n      <div class=\"sel-wrap\">\n        <select id=\"selYear\"><option value=\"\">Select Year<\/option><\/select>\n      <\/div>\n    <\/div>\n\n    <div class=\"sel-group\" style=\"max-width:220px;\">\n      <label>Assign To<span class=\"req\">*<\/span><\/label>\n      <div class=\"sel-wrap\">\n        <select id=\"selUser\"><option value=\"\">Select User<\/option><\/select>\n      <\/div>\n    <\/div>\n\n    <div class=\"btn-align\">\n      <button class=\"btn btn-primary\" id=\"searchBtn\" onclick=\"handleSearch()\">Search<\/button>\n    <\/div>\n  <\/div>\n\n  <!-- CSV DOWNLOAD (shown after first search) -->\n  <div class=\"csv-row\" id=\"csvRow\" style=\"display:none;\">\n    <button class=\"btn btn-csv\" onclick=\"downloadCSV()\">CSV Download<\/button>\n  <\/div>\n\n  <!-- TABS -->\n  <div class=\"tab-bar\" style=\"margin-top:24px;\">\n    <div class=\"tab-item active\" id=\"tabOperational\" onclick=\"switchTab('operational')\">Operational<\/div>\n    <div class=\"tab-item\"        id=\"tabQC\"          onclick=\"switchTab('qc')\">QC<\/div>\n  <\/div>\n\n  <!-- RESULTS SECTION (hidden until search) -->\n  <div class=\"results-section\" id=\"resultsSection\">\n\n    <!-- Reverse Sign Off Hit banner -->\n    <div style=\"margin-top:16px;\">\n      <span class=\"rsof-banner\">Reverse Sign Off Hit<\/span>\n    <\/div>\n\n    <!-- Monthly Productivity -->\n    <div class=\"section-title\" style=\"margin-top:10px;\">Monthly Productivity<\/div>\n    <div class=\"stat-cards\">\n      <div class=\"stat-card\"><div class=\"s-label\">Total Signoff Hits<\/div><div class=\"s-value\" id=\"sc-signoff\">0<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Total<br>Productivity Hits<\/div><div class=\"s-value\" id=\"sc-prod-hit\">0<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Total Hours Utilize<\/div><div class=\"s-value\" id=\"sc-hrs-util\">0.00<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Total Adhoc Utilize<\/div><div class=\"s-value\" id=\"sc-adhoc-util\">0.00<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Total Hours<\/div><div class=\"s-value\" id=\"sc-hrs-total\">0.00<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Productivity %<\/div><div class=\"s-value red\" id=\"sc-prod-pct\">0.00%<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Quality %<\/div><div class=\"s-value green\" id=\"sc-quality-pct\">100.00%<\/div><\/div>\n    <\/div>\n\n    <!-- Till Date Productivity (QC tab only) -->\n    <div id=\"tillDateSection\" style=\"display:none;\">\n      <div class=\"section-title\" id=\"tillDateTitle\">Till Date Productivity<\/div>\n      <div class=\"stat-cards\">\n        <div class=\"stat-card\"><div class=\"s-label\">Total Signoff Hits<\/div><div class=\"s-value\" id=\"td-signoff\">0<\/div><\/div>\n        <div class=\"stat-card\"><div class=\"s-label\">Total<br>Productivity Hits<\/div><div class=\"s-value\" id=\"td-prod-hit\">0<\/div><\/div>\n        <div class=\"stat-card\"><div class=\"s-label\">Total Hours Utilize<\/div><div class=\"s-value\" id=\"td-hrs-util\">0.00<\/div><\/div>\n        <div class=\"stat-card\"><div class=\"s-label\">Total Adhoc Utilize<\/div><div class=\"s-value\" id=\"td-adhoc-util\">0.00<\/div><\/div>\n        <div class=\"stat-card\"><div class=\"s-label\">Total Hours<\/div><div class=\"s-value\" id=\"td-hrs-total\">0.00<\/div><\/div>\n        <div class=\"stat-card\"><div class=\"s-label\">Productivity %<\/div><div class=\"s-value red\" id=\"td-prod-pct\">0.00%<\/div><\/div>\n      <\/div>\n    <\/div>\n\n  <\/div><!-- \/resultsSection -->\n\n  <!-- TABLE WRAPPER -->\n  <div class=\"table-wrapper\">\n    <table>\n      <thead>\n        <tr>\n          <th>Assigned Date<\/th>\n          <th>Assigned To<\/th>\n          <th>Pesudo Name<\/th>\n          <th>Assigned DOS<\/th>\n          <th>DOS Completed<\/th>\n          <th>Pending DOS<\/th>\n          <th>Hrs Utilized<\/th>\n          <th>Ad Hoc Hrs<\/th>\n          <th>Productivity Hit<\/th>\n          <th>Sign Off Hit<\/th>\n          <th>Attendance<\/th>\n        <\/tr>\n      <\/thead>\n      <tbody id=\"productivityBody\">\n        <tr class=\"state-row\"><td colspan=\"11\">No Data Found<\/td><\/tr>\n      <\/tbody>\n    <\/table>\n  <\/div>\n\n  <!-- PAGINATION -->\n  <div class=\"pagination-footer\" id=\"paginationFooter\" style=\"display:none;\">\n    <div class=\"pagination-controls\">\n      <span class=\"nav-btn disabled\" id=\"prevBtn\" onclick=\"changePage(-1)\">\u00ab Previous<\/span>\n      <div id=\"pageNumbers\" style=\"display:flex;gap:6px;\"><\/div>\n      <span class=\"nav-btn disabled\" id=\"nextBtn\" onclick=\"changePage(1)\">Next \u00bb<\/span>\n    <\/div>\n    <div id=\"entryInfo\">Showing 0 to 0 of 0 entries<\/div>\n  <\/div>\n\n<\/div><!-- \/page-wrapper -->\n\n<script>\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n   API ENDPOINTS\n   \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n   PRODUCTIVITY_BY_USER    : GET \/productivity\/byUserId\n       Params : userId (number), month (number), year (number)\n       Used   : Operational tab \u2014 table rows + stat cards\n\n   QC_PRODUCTIVITY_BY_USER : GET \/productivity\/qc-byUserId\n       Params : userId (number), month (number), year (number)\n       Used   : QC tab \u2014 table rows + stat cards\n\n   QC_USER_LIST            : GET \/user\/qc-list\n       Params : none\n       Used   : \"Assign To\" dropdown on QC tab\n\n   USER_TABLE_LIST         : GET \/user\/table\/list\n       Params : page (number), limit (number)\n       Used   : \"Assign To\" dropdown on Operational tab\n                (all pages fetched in parallel automatically)\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\nconst BASE_URL = 'https:\/\/api-ph-portal.zyneventures.com\/api\/v1';\nconst ENDPOINTS = {\n  PRODUCTIVITY_BY_USER:    `${BASE_URL}\/productivity\/byUserId`,\n  QC_PRODUCTIVITY_BY_USER: `${BASE_URL}\/productivity\/qc-byUserId`,\n  QC_USER_LIST:            `${BASE_URL}\/user\/qc-list`,\n  USER_TABLE_LIST:         `${BASE_URL}\/user\/table\/list`\n};\n\n\/* STATE *\/\nlet activeTab    = 'operational';\nlet allUsers     = [];   \/\/ from \/user\/table\/list  \u2192 Operational dropdown\nlet qcUsers      = [];   \/\/ from \/user\/qc-list     \u2192 QC dropdown\nlet currentData  = [];\nlet currentStats = null;\nlet pagination   = { page: 1, limit: 20, total: 0, pages: 0 };\n\n\/* AUTH *\/\nconst getHeaders = () => ({\n  'Content-Type':  'application\/json',\n  'Authorization': `Bearer ${localStorage.getItem('TOKEN')}`\n});\n\n\/* \u2500\u2500 INIT \u2500\u2500 *\/\ndocument.addEventListener('DOMContentLoaded', () => {\n  populateYears();\n  loadUsers();\n});\n\nfunction populateYears() {\n  const sel = document.getElementById('selYear');\n  const cur = new Date().getFullYear();\n  for (let y = cur; y >= 2020; y--) {\n    const o = document.createElement('option');\n    o.value = y; o.textContent = y; sel.appendChild(o);\n  }\n}\n\nasync function loadUsers() {\n  await Promise.all([loadOperationalUsers(), loadQcUsers()]);\n  populateUserDropdown();\n}\n\n\/* GET \/user\/table\/list?page=<n>&limit=100  \u2014 Operational \"Assign To\" *\/\nasync function loadOperationalUsers() {\n  try {\n    allUsers = [];\n    const r1   = await fetch(`${ENDPOINTS.USER_TABLE_LIST}?page=1&limit=100`, { headers: getHeaders() });\n    const d1   = await r1.json();\n    if (!d1.data || !Array.isArray(d1.data)) return;\n    allUsers = allUsers.concat(d1.data);\n    const total = d1.pagination ? d1.pagination.total : d1.data.length;\n    const pages = Math.ceil(total \/ 100);\n    const reqs  = [];\n    for (let p = 2; p <= pages; p++)\n      reqs.push(fetch(`${ENDPOINTS.USER_TABLE_LIST}?page=${p}&#038;limit=100`, { headers: getHeaders() }).then(r => r.json()));\n    const rest = await Promise.all(reqs);\n    rest.forEach(d => { if (d.data && Array.isArray(d.data)) allUsers = allUsers.concat(d.data); });\n  } catch (e) { console.error('loadOperationalUsers:', e); }\n}\n\n\/* GET \/user\/qc-list  \u2014 QC \"Assign To\" *\/\nasync function loadQcUsers() {\n  try {\n    const r = await fetch(ENDPOINTS.QC_USER_LIST, { headers: getHeaders() });\n    const d = await r.json();\n    if (d.data && Array.isArray(d.data)) qcUsers = d.data;\n  } catch (e) { console.error('loadQcUsers:', e); }\n}\n\nfunction populateUserDropdown() {\n  const sel   = document.getElementById('selUser');\n  const users = activeTab === 'qc' ? qcUsers : allUsers;\n  sel.innerHTML = '<option value=\"\">Select User<\/option>';\n  users.forEach(u => {\n    const o = document.createElement('option');\n    o.value = u.id; o.textContent = u.pesudo_name || u.name;\n    sel.appendChild(o);\n  });\n}\n\n\/* \u2500\u2500 TAB SWITCHING \u2500\u2500 *\/\nfunction switchTab(tab) {\n  activeTab = tab;\n  document.getElementById('tabOperational').classList.toggle('active', tab === 'operational');\n  document.getElementById('tabQC').classList.toggle('active', tab === 'qc');\n\n  clearTable();\n  document.getElementById('resultsSection').classList.remove('visible');\n  document.getElementById('csvRow').style.display       = 'none';\n  document.getElementById('paginationFooter').style.display = 'none';\n\n  document.getElementById('selUser').value = '';\n  populateUserDropdown();\n}\n\n\/* \u2500\u2500 SEARCH \u2500\u2500 *\/\nasync function handleSearch() {\n  const month  = document.getElementById('selMonth').value;\n  const year   = document.getElementById('selYear').value;\n  const userId = document.getElementById('selUser').value;\n\n  if (!month || !year || !userId) {\n    alert('Please select Month, Year and Assign To before searching.');\n    return;\n  }\n\n  const btn = document.getElementById('searchBtn');\n  btn.disabled = true; btn.textContent = 'Searching\u2026';\n  try {\n    if (activeTab === 'operational') await fetchOperationalData(userId, month, year);\n    else                             await fetchQcData(userId, month, year);\n  } finally {\n    btn.disabled = false; btn.textContent = 'Search';\n  }\n}\n\n\/* GET \/productivity\/byUserId?userId=<n>&month=<n>&year=<n> *\/\nasync function fetchOperationalData(userId, month, year) {\n  showLoading();\n  try {\n    const url  = `${ENDPOINTS.PRODUCTIVITY_BY_USER}?userId=${userId}&month=${month}&year=${year}`;\n    const res  = await fetch(url, { headers: getHeaders() });\n    const json = await res.json();\n    if (json.code === 200 && json.data) processAndRender(json.data, false);\n    else clearTable();\n  } catch (e) { console.error('fetchOperationalData:', e); clearTable(); }\n}\n\nasync function fetchQcData(userId, month, year) {\n  showLoading();\n  try {\n    const url  = `${ENDPOINTS.QC_PRODUCTIVITY_BY_USER}?userId=${userId}&month=${month}&year=${year}`;\n    const res  = await fetch(url, { headers: getHeaders() });\n    const json = await res.json();\n    if (json.code === 200 && json.data) processAndRender(json.data, true);\n    else clearTable();\n  } catch (e) { console.error('fetchQcData:', e); clearTable(); }\n}\n\n\/* \u2500\u2500 PROCESS & RENDER \u2500\u2500 *\/\nfunction processAndRender(data, isQC) {\n  currentData  = Object.values(data.productivity || {});\n  currentStats = data.stats || null;\n\n  pagination.total = currentData.length;\n  pagination.pages = Math.max(1, Math.ceil(currentData.length \/ pagination.limit));\n  pagination.page  = 1;\n\n  updateStatCards(currentStats, isQC);\n  document.getElementById('resultsSection').classList.add('visible');\n  document.getElementById('csvRow').style.display = 'block';\n\n  renderTable();\n  renderPagination();\n\n  document.getElementById('paginationFooter').style.display =\n    currentData.length > 0 ? 'flex' : 'none';\n}\n\n\/* \u2500\u2500 STAT CARDS \u2500\u2500 *\/\nfunction fmt2(v) {\n  if (v === null || v === undefined || v === '') return '0.00';\n  const n = parseFloat(v); return isNaN(n) ? String(v) : n.toFixed(2);\n}\n\nfunction updateStatCards(stats, isQC) {\n  if (!stats) return;\n\n  document.getElementById('sc-signoff').textContent    = fmt2(stats.signoff_hits);\n  document.getElementById('sc-prod-hit').textContent   = fmt2(stats.productivity_hit);\n  document.getElementById('sc-hrs-util').textContent   = fmt2(stats.hours_utilize);\n  document.getElementById('sc-adhoc-util').textContent = fmt2(stats.adhoc_utilize);\n  document.getElementById('sc-hrs-total').textContent  = fmt2(stats.hours_total);\n  document.getElementById('sc-prod-pct').textContent   = fmt2(stats.productivity) + '%';\n\n  const quality = stats.quality_percentage !== undefined ? fmt2(stats.quality_percentage) : '100.00';\n  document.getElementById('sc-quality-pct').textContent = quality + '%';\n\n  const tillSection = document.getElementById('tillDateSection');\n  if (isQC) {\n    const today = new Date().toISOString().split('T')[0];\n    document.getElementById('tillDateTitle').textContent    = `Till Date Productivity (${today})`;\n    document.getElementById('td-signoff').textContent       = fmt2(stats.signoff_hits);\n    document.getElementById('td-prod-hit').textContent      = fmt2(stats.productivity_hit);\n    document.getElementById('td-hrs-util').textContent      = fmt2(stats.hours_utilize);\n    document.getElementById('td-adhoc-util').textContent    = fmt2(stats.adhoc_utilize);\n    document.getElementById('td-hrs-total').textContent     = fmt2(stats.hours_total);\n    document.getElementById('td-prod-pct').textContent      = fmt2(stats.productivity) + '%';\n    tillSection.style.display = 'block';\n  } else {\n    tillSection.style.display = 'none';\n  }\n}\n\n\/* \u2500\u2500 RENDER TABLE \u2500\u2500 *\/\nfunction renderTable() {\n  const tbody = document.getElementById('productivityBody');\n  tbody.innerHTML = '';\n\n  if (!currentData || currentData.length === 0) {\n    tbody.innerHTML = `<tr class=\"state-row\"><td colspan=\"11\">No Data Found<\/td><\/tr>`;\n    return;\n  }\n\n  const start = (pagination.page - 1) * pagination.limit;\n  const end   = Math.min(start + pagination.limit, currentData.length);\n  const slice = currentData.slice(start, end);\n\n  slice.forEach(row => {\n    const attendance = parseInt(row.attendance);\n    const attBadge   = (attendance === 1)\n      ? `<span class=\"badge-present\">Present<\/span>`\n      : `<span class=\"badge-absent\">Absent<\/span>`;\n\n    const tr = document.createElement('tr');\n    tr.innerHTML = `\n      <td>${row.assigned_date || '\u2014'}<\/td>\n      <td>${row.assigned_to   || '\u2014'}<\/td>\n      <td>${row.pesudo_name   || '\u2014'}<\/td>\n      <td>${row.assigned_dos  ?? 0}<\/td>\n      <td>${row.complete_dos  ?? 0}<\/td>\n      <td>${row.pending_dos   ?? 0}<\/td>\n      <td>${fmt2(row.hours)}<\/td>\n      <td>${fmt2(row.adHours)}<\/td>\n      <td>${row.productivity_hit ?? 0}<\/td>\n      <td>${row.signoff_hit      ?? 0}<\/td>\n      <td>${attBadge}<\/td>\n    `;\n    tbody.appendChild(tr);\n  });\n\n  const displayStart = pagination.total === 0 ? 0 : start + 1;\n  document.getElementById('entryInfo').textContent =\n    `Showing ${displayStart} to ${end} of ${pagination.total} entries`;\n}\n\n\/* \u2500\u2500 PAGINATION \u2500\u2500 *\/\nfunction renderPagination() {\n  const container = document.getElementById('pageNumbers');\n  container.innerHTML = '';\n  const { page, pages } = pagination;\n\n  document.getElementById('prevBtn').className = page <= 1     ? 'nav-btn disabled' : 'nav-btn';\n  document.getElementById('nextBtn').className = page >= pages ? 'nav-btn disabled' : 'nav-btn';\n\n  let nums = [];\n  if (pages <= 7) { for (let i = 1; i <= pages; i++) nums.push(i); }\n  else {\n    nums = [1];\n    if (page > 3) nums.push('\u2026');\n    for (let i = Math.max(2, page - 1); i <= Math.min(pages - 1, page + 1); i++) nums.push(i);\n    if (page < pages - 2) nums.push('\u2026');\n    nums.push(pages);\n  }\n\n  nums.forEach(n => {\n    if (n === '\u2026') {\n      const s = document.createElement('span');\n      s.textContent = '\u2026'; s.style.cssText = 'display:flex;align-items:center;padding:0 4px;color:#aaa;';\n      container.appendChild(s);\n    } else {\n      const s = document.createElement('span');\n      s.className = n === page ? 'page-num active' : 'page-num';\n      s.textContent = n; s.onclick = () => goToPage(n);\n      container.appendChild(s);\n    }\n  });\n}\n\nfunction goToPage(n) { pagination.page = n; renderTable(); renderPagination(); }\nfunction changePage(dir) {\n  const np = pagination.page + dir;\n  if (np < 1 || np > pagination.pages) return;\n  pagination.page = np; renderTable(); renderPagination();\n}\n\n\/* \u2500\u2500 CSV DOWNLOAD \u2500\u2500 *\/\nfunction downloadCSV() {\n  const heads = ['Assigned Date','Assigned To','Pesudo Name','Assigned DOS','DOS Completed',\n                 'Pending DOS','Hrs Utilized','Ad Hoc Hrs','Productivity Hit','Sign Off Hit','Attendance'];\n  const rows = [...document.querySelectorAll('#productivityBody tr:not(.state-row)')];\n  if (!rows.length) { alert('No data to download.'); return; }\n  let csv = heads.join(',') + '\\n';\n  rows.forEach(tr => {\n    const cells = [...tr.querySelectorAll('td')].map(td => '\"' + td.textContent.trim().replace(\/\"\/g,'\"\"') + '\"');\n    csv += cells.join(',') + '\\n';\n  });\n  const a = document.createElement('a');\n  a.href = 'data:text\/csv;charset=utf-8,' + encodeURIComponent(csv);\n  a.download = `productivity_${activeTab}_${Date.now()}.csv`;\n  a.click();\n}\n\n\/* \u2500\u2500 HELPERS \u2500\u2500 *\/\nfunction showLoading() {\n  document.getElementById('productivityBody').innerHTML =\n    `<tr class=\"state-row\"><td colspan=\"11\"><span class=\"spinner\"><\/span>Loading\u2026<\/td><\/tr>`;\n  document.getElementById('paginationFooter').style.display = 'none';\n}\n\nfunction clearTable() {\n  currentData = []; currentStats = null;\n  document.getElementById('productivityBody').innerHTML =\n    `<tr class=\"state-row\"><td colspan=\"11\">No Data Found<\/td><\/tr>`;\n  document.getElementById('paginationFooter').style.display = 'none';\n}\n<\/script>\n<\/div><\/div>\n\n<\/div><\/div><\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>PRODUCTIVITY LIST Month* Select MonthJanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctoberNovemberDecember Year* Select Year Assign To* Select User Search CSV Download Operational QC Reverse Sign Off Hit Monthly Productivity Total Signoff Hits 0 TotalProductivity Hits 0 Total Hours Utilize 0.00 Total Adhoc Utilize 0.00 Total Hours 0.00 Productivity % 0.00% Quality % 100.00% Till Date Productivity Total Signoff Hits 0 TotalProductivity&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"class_list":["post-146","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages\/146","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/comments?post=146"}],"version-history":[{"count":2,"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages\/146\/revisions"}],"predecessor-version":[{"id":153,"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages\/146\/revisions\/153"}],"wp:attachment":[{"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/media?parent=146"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}