{"id":146,"date":"2026-03-31T07:01:05","date_gmt":"2026-03-31T07:01:05","guid":{"rendered":"https:\/\/ph-taskportal.zyneventures.com\/?page_id=146"},"modified":"2026-07-09T09:36:25","modified_gmt":"2026-07-09T09:36:25","slug":"productivity","status":"publish","type":"page","link":"https:\/\/ph-portal-v2.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<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Productivity List<\/title>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Sora:wght@300;400;500;600&amp;display=swap\" rel=\"stylesheet\">\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        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\n    \/* CSV Download button *\/\n    .btn-csv {\n      background: var(--primary-blue);\n      color: #fff;\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      display: none;\n    }\n    .btn-csv:hover { background: var(--hover-blue); }\n    .btn-csv.visible { display: inline-block; }\n\n    .btn-align { display: flex; align-items: flex-end; gap: 10px; }\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      display: inline-block;\n      border: none;\n      cursor: pointer;\n      font-family: 'Sora', sans-serif;\n      transition: opacity .2s;\n    }\n    .rsof-banner:disabled { opacity: .65; cursor: not-allowed; }\n\n    \/* REVERSE PRODUCTIVITY HIT BANNER *\/\n    .rprod-banner {\n      background: #dc3545; color: #fff;\n      font-size: 12px; \n      font-weight: 400;\n      padding: 8px 18px; \n      border-radius: 6px;\n      display: inline-block;\n      border: none;\n      cursor: pointer;\n      font-family: 'Sora', sans-serif;\n      transition: opacity .2s;\n    }\n    .rprod-banner:hover:not(:disabled) { opacity: .9; }\n    .rprod-banner:disabled { opacity: .65; cursor: not-allowed; }\n\n    \/* Banner row \u2014 holds both reverse buttons side by side *\/\n    .banner-row {\n      display: flex;\n      flex-wrap: wrap;\n      gap: 10px;\n      align-items: center;\n      margin-top: 16px;\n      margin-bottom: 0;\n    }\n    .banner-row.hidden { display: none; }\n    .reverse-hit-btn.hidden { display: none; }\n\n    \/* SECTION TITLE *\/\n    .section-title { \n        font-size: 28px; \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: #f2f2f2; \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: 13px; \n      font-weight: 400;\n      color: var(--text-muted); \n      margin-bottom: 6px; \n      white-space: nowrap; \n    }\n    .stat-card .s-value { font-size: 25px; 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      max-width: 100%;\n      -webkit-overflow-scrolling: touch;\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 {\n      width: 100%;\n      max-width: 100%;\n      border-collapse: collapse;\n      table-layout: fixed;\n      font-size: 10px;\n      min-width: 0;\n    }\n    thead tr { background: #f5f5f5; }\n    thead th {\n      padding: 8px 5px; \n      font-weight: 500; \n      font-size: 9px; \n      text-transform: uppercase;\n      letter-spacing: .3px; \n      color: #212529; \n      border-bottom: 1px solid var(--border-color);\n      white-space: normal;\n      word-break: normal;\n      overflow-wrap: anywhere;\n      line-height: 1.25;\n      text-align: left;\n      vertical-align: middle;\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 {\n      padding: 8px 5px;\n      color: var(--text-dark);\n      vertical-align: middle;\n      font-size: 9px;\n      line-height: 1.25;\n      white-space: normal;\n      word-break: break-word;\n      overflow-wrap: anywhere;\n    }\n\n    thead th:nth-child(1),\n    tbody td:nth-child(1),\n    thead th:nth-child(11),\n    tbody td:nth-child(11) {\n      white-space: nowrap;\n      word-break: normal;\n      overflow-wrap: normal;\n    }\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      cursor: pointer; \n      font-size: 12px;\n      background: transparent; \n      color: #5f6368; \n      user-select: none; \n      transition: background .2s;\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      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\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\" id=\"assignToGroup\" 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      <button class=\"btn-csv\" id=\"csvDownloadBtn\" onclick=\"handleCsvDownload()\">CSV Download<\/button>\n    <\/div>\n  <\/div>\n\n  <!-- TABS -->\n  <div class=\"tab-bar\" style=\"margin-top:12px;\">\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  <!-- Reverse hit buttons \u2014 between tabs and Monthly Productivity (API-driven visibility) -->\n  <div class=\"banner-row hidden\" id=\"reverseBannerRow\">\n    <button type=\"button\" class=\"rprod-banner reverse-hit-btn hidden\" id=\"reverseProdHitBtn\" onclick=\"handleReverseProdHit()\">Reverse Productivity Hit<\/button>\n    <button type=\"button\" class=\"rsof-banner reverse-hit-btn hidden\" id=\"reverseHitBtn\" onclick=\"handleReverseHit()\">Reverse Sign Off Hit<\/button>\n  <\/div>\n\n  <!-- RESULTS SECTION (hidden until search) -->\n  <div class=\"results-section\" id=\"resultsSection\">\n\n    <!-- Monthly Productivity -->\n    <div class=\"section-title\" style=\"margin-top:10px;\">Monthly Productivity<\/div>\n\n    <!-- Operational stat cards (with Quality %) -->\n    <div class=\"stat-cards\" id=\"operationalStatCards\">\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    <!-- QC stat cards (with Quality %) -->\n    <div class=\"stat-cards\" id=\"qcStatCards\" style=\"display:none;\">\n      <div class=\"stat-card\"><div class=\"s-label\">Total Signoff Hits<\/div><div class=\"s-value\" id=\"qc-sc-signoff\">0<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Total<br>Productivity Hits<\/div><div class=\"s-value\" id=\"qc-sc-prod-hit\">0<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Total Hours Utilize<\/div><div class=\"s-value\" id=\"qc-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=\"qc-sc-adhoc-util\">0.00<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Total Hours<\/div><div class=\"s-value\" id=\"qc-sc-hrs-total\">0.00<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Productivity %<\/div><div class=\"s-value red\" id=\"qc-sc-prod-pct\">0.00%<\/div><\/div>\n      <div class=\"stat-card\"><div class=\"s-label\">Quality %<\/div><div class=\"s-value green\" id=\"qc-sc-quality-pct\">100.00%<\/div><\/div>\n    <\/div>\n\n    <!-- Till Date Productivity (both tabs) -->\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   TEAM_MEMBERS            : GET \/user\/team-members\n       Params : none\n       Used   : \"Assign To\" dropdown on Operational tab only.\n                Returns all team members in a single response; each\n                record exposes `pesudo_name` which is shown in the\n                dropdown label.\n\n   QC_LIST                 : GET \/user\/qc-list\n       Params : none\n       Used   : \"Assign To\" dropdown on QC tab only.\n                Returns QC users; each record exposes `pesudo_name`\n                which is shown in the dropdown label.\n\n   REVERSE_HIT             : POST \/productivity\/reverse-hit\n       Body   : reverse_hit_by, user_id, month, year, type\n                type = 'productivity' | 'sign_off'\n       Used   : \"Reverse Productivity Hit\" and \"Reverse Sign Off Hit\"\n                buttons \u2014 shown when API returns\n                show_reverse_productivity_hit_button \/\n                show_reverse_signoff_hit_button as true\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  TEAM_MEMBERS:            `${BASE_URL}\/user\/team-members`,\n  QC_LIST:                 `${BASE_URL}\/user\/qc-list`,\n  REVERSE_HIT:             `${BASE_URL}\/productivity\/reverse-hit`\n};\n\n\/* STATE *\/\nlet activeTab    = 'operational';\nlet allUsers     = [];   \/\/ from \/user\/team-members \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 ROLE-BASED UI: hide \"Assign To\" filter for ASSOCIATE_ROLES \u2500\u2500\n   Source of truth: Login.html sets PERMISSION_TIER ('admin' | 'manager' |\n   'teamlead' | 'associate' | 'none') in localStorage based on USER_ROLE.\n   Primary check uses PERMISSION_TIER. Fallback re-evaluates USER_ROLE\n   against the same ASSOCIATE_ROLES list used in Login.html, so sessions\n   that pre-date PERMISSION_TIER being stored still behave correctly. *\/\nconst ASSOCIATE_ROLES = [\n  \"intern\",\n  \"accounts executive - i\",\n  \"accounts executive - ii\",\n  \"accounts executive - iii\",\n  \"account executive\",\n  \"account executive-ii\",\n  \"account executive-iii\",\n  \"senior accounts executive i\",\n  \"senior accounts executive ii\",\n  \"senoir account executive-iii\"\n];\n\n\/** PHP productivity\/list.blade.php \u2014 reverse buttons hidden for these roles *\/\nconst ACCOUNTS_EXECUTIVE_ROLES = [\n  \"accounts executive\",\n  \"accounts executive - i\",\n  \"accounts executive - ii\",\n  \"accounts executive - iii\",\n  \"senior accounts executive i\",\n  \"senior accounts executive ii\",\n];\n\nfunction isAccountsExecutiveUser() {\n  const role = (localStorage.getItem('USER_ROLE') || '').trim().toLowerCase();\n  return ACCOUNTS_EXECUTIVE_ROLES.includes(role);\n}\n\nfunction isAssociateUser() {\n  const tier = (localStorage.getItem('PERMISSION_TIER') || '').trim().toLowerCase();\n  if (tier === 'associate') return true;\n\n  const role = (localStorage.getItem('USER_ROLE') || '').trim().toLowerCase();\n  return ASSOCIATE_ROLES.includes(role);\n}\n\nfunction applyRoleBasedUI() {\n  if (isAssociateUser()) {\n    const grp = document.getElementById('assignToGroup');\n    if (grp) grp.style.display = 'none';\n  }\n  if (isAccountsExecutiveUser()) {\n    hideReverseHitButtons();\n  }\n}\n\n\/* \u2500\u2500 CURRENT MONTH CHECK \u2500\u2500\n   Returns true only when the selected month matches the current\n   calendar month (regardless of the selected year). *\/\nfunction isCurrentMonthSelected() {\n  const selectedMonth = parseInt(document.getElementById('selMonth').value, 10);\n  if (!selectedMonth) return false;\n  const currentMonth = new Date().getMonth() + 1; \/\/ getMonth() is 0-indexed\n  return selectedMonth === currentMonth;\n}\n\n\/* \u2500\u2500 INIT \u2500\u2500 *\/\ndocument.addEventListener('DOMContentLoaded', async () => {\n  applyRoleBasedUI();\n  populateYears();\n  await loadUsers();\n\n  \/* \u2500\u2500 AUTO-SEARCH when redirected from User.html Monthly % link \u2500\u2500\n     URL params expected: ?id=<userId>&month=<1-12>&year=<YYYY>\n     When present, pre-fill the filters and trigger search automatically\n     so only that specific user's data is displayed on screen.            *\/\n  const urlParams  = new URLSearchParams(window.location.search);\n  const paramId    = urlParams.get('id');\n  const paramMonth = urlParams.get('month');\n  const paramYear  = urlParams.get('year');\n\n  if (paramId && paramMonth && paramYear) {\n    \/\/ Pre-fill Month and Year dropdowns\n    document.getElementById('selMonth').value = paramMonth;\n    document.getElementById('selYear').value  = paramYear;\n\n    \/\/ Pre-select the user in the Assign To dropdown\n    const selUser = document.getElementById('selUser');\n    selUser.value = paramId;\n\n    \/\/ If the user id is not yet in the dropdown (edge case), add a\n    \/\/ temporary placeholder option so handleSearch() can read the value\n    if (selUser.value !== paramId) {\n      const opt = document.createElement('option');\n      opt.value = paramId;\n      opt.textContent = `User #${paramId}`;\n      selUser.appendChild(opt);\n      selUser.value = paramId;\n    }\n\n    \/\/ Trigger search \u2014 renders only this user's productivity data\n    await handleSearch();\n  }\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\/team-members  \u2014 Operational \"Assign To\"\n   Returns all team members in a single call (no pagination needed). *\/\nasync function loadOperationalUsers() {\n  try {\n    allUsers = [];\n    const r = await fetch(ENDPOINTS.TEAM_MEMBERS, { headers: getHeaders() });\n    const d = await r.json();\n    if (d.data && Array.isArray(d.data)) allUsers = d.data;\n  } catch (e) { console.error('loadOperationalUsers:', e); }\n}\n\n\/* GET \/user\/qc-list  \u2014 QC \"Assign To\"\n   Returns QC users specifically for the QC tab dropdown. *\/\nasync function loadQcUsers() {\n  try {\n    qcUsers = [];\n    const r = await fetch(ENDPOINTS.QC_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  hideReverseHitButtons();\n  document.getElementById('resultsSection').classList.remove('visible');\n  document.getElementById('paginationFooter').style.display = 'none';\n\n  \/\/ Hide CSV button when tab is switched\n  document.getElementById('csvDownloadBtn').classList.remove('visible');\n\n  document.getElementById('selUser').value = '';\n  populateUserDropdown();\n}\n\n\/* \u2500\u2500 REVERSE SIGN OFF HIT \u2500\u2500 *\/\nasync function handleReverseHit() {\n  const month  = document.getElementById('selMonth').value;\n  const year   = document.getElementById('selYear').value;\n  let   userId = document.getElementById('selUser').value;\n\n  if (isAssociateUser()) {\n    userId = localStorage.getItem('USER_ID') || '';\n    if (!month || !year) {\n      alert('Please select Month and Year before reversing sign off hits.');\n      return;\n    }\n    if (!userId) {\n      alert('Unable to identify the logged-in user. Please log in again.');\n      return;\n    }\n  } else {\n    if (!month || !year || !userId) {\n      alert('Please select Month, Year and Assign To before reversing sign off hits.');\n      return;\n    }\n  }\n\n  const reverseHitBy = localStorage.getItem('USER_ID');\n  if (!reverseHitBy) {\n    alert('Unable to identify the logged-in user. Please log in again.');\n    return;\n  }\n\n  if (!confirm('Are you sure you want to reverse sign off hits for the selected user?')) return;\n\n  const btn = document.getElementById('reverseHitBtn');\n  btn.disabled = true;\n  const prevText = btn.textContent;\n  btn.textContent = 'Processing\u2026';\n\n  try {\n    const res = await fetch(ENDPOINTS.REVERSE_HIT, {\n      method:  'POST',\n      headers: getHeaders(),\n      body:    JSON.stringify({\n        reverse_hit_by: Number(reverseHitBy),\n        user_id:        Number(userId),\n        month:          String(month).padStart(2, '0'),\n        year:           String(year),\n        type:           'sign_off'\n      })\n    });\n\n    const json = await res.json();\n\n    if (json.status === 'success' && json.code === 200) {\n      const msg = (json.messages && json.messages[0]) || 'Reverse Hits Completed Successfully';\n      alert(msg);\n      if (document.getElementById('resultsSection').classList.contains('visible')) {\n        await handleSearch();\n      }\n    } else {\n      const errMsg = (json.messages && json.messages.join(', ')) ||\n                     (json.errors && (Array.isArray(json.errors) ? json.errors.join(', ') : String(json.errors))) ||\n                     'Reverse hit failed. Please try again.';\n      alert(errMsg);\n    }\n  } catch (e) {\n    console.error('handleReverseHit:', e);\n    alert('Network error. Please try again.');\n  } finally {\n    btn.disabled = false;\n    btn.textContent = prevText;\n  }\n}\n\n\/* \u2500\u2500 REVERSE PRODUCTIVITY HIT (QC tab) \u2500\u2500 *\/\nasync function handleReverseProdHit() {\n  const month  = document.getElementById('selMonth').value;\n  const year   = document.getElementById('selYear').value;\n  let   userId = document.getElementById('selUser').value;\n\n  if (isAssociateUser()) {\n    userId = localStorage.getItem('USER_ID') || '';\n    if (!month || !year) {\n      alert('Please select Month and Year before reversing productivity hits.');\n      return;\n    }\n    if (!userId) {\n      alert('Unable to identify the logged-in user. Please log in again.');\n      return;\n    }\n  } else {\n    if (!month || !year || !userId) {\n      alert('Please select Month, Year and Assign To before reversing productivity hits.');\n      return;\n    }\n  }\n\n  const reverseHitBy = localStorage.getItem('USER_ID');\n  if (!reverseHitBy) {\n    alert('Unable to identify the logged-in user. Please log in again.');\n    return;\n  }\n\n  if (!confirm('Are you sure you want to reverse productivity hits for the selected user?')) return;\n\n  const btn = document.getElementById('reverseProdHitBtn');\n  btn.disabled = true;\n  const prevText = btn.textContent;\n  btn.textContent = 'Processing\u2026';\n\n  try {\n    const res = await fetch(ENDPOINTS.REVERSE_HIT, {\n      method:  'POST',\n      headers: getHeaders(),\n      body:    JSON.stringify({\n        reverse_hit_by: Number(reverseHitBy),\n        user_id:        Number(userId),\n        month:          String(month).padStart(2, '0'),\n        year:           String(year),\n        type:           'productivity'\n      })\n    });\n\n    const json = await res.json();\n\n    if (json.status === 'success' && json.code === 200) {\n      const msg = (json.messages && json.messages[0]) || 'Reverse Productivity Hits Completed Successfully';\n      alert(msg);\n      if (document.getElementById('resultsSection').classList.contains('visible')) {\n        await handleSearch();\n      }\n    } else {\n      const errMsg = (json.messages && json.messages.join(', ')) ||\n                     (json.errors && (Array.isArray(json.errors) ? json.errors.join(', ') : String(json.errors))) ||\n                     'Reverse productivity hit failed. Please try again.';\n      alert(errMsg);\n    }\n  } catch (e) {\n    console.error('handleReverseProdHit:', e);\n    alert('Network error. Please try again.');\n  } finally {\n    btn.disabled = false;\n    btn.textContent = prevText;\n  }\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  let   userId = document.getElementById('selUser').value;\n\n  \/\/ ASSOCIATE_ROLES: \"Assign To\" is hidden, so userId is auto-sourced\n  \/\/ from the logged-in user's USER_ID (set by Login.html). Only Month\n  \/\/ and Year are required for associates.\n  if (isAssociateUser()) {\n    userId = localStorage.getItem('USER_ID') || '';\n    if (!month || !year) {\n      alert('Please select Month and Year before searching.');\n      return;\n    }\n    if (!userId) {\n      alert('Unable to identify the logged-in user. Please log in again.');\n      return;\n    }\n  } else {\n    if (!month || !year || !userId) {\n      alert('Please select Month, Year and Assign To before searching.');\n      return;\n    }\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, json);\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, json);\n    else clearTable();\n  } catch (e) { console.error('fetchQcData:', e); clearTable(); }\n}\n\n\/* \u2500\u2500 PROCESS & RENDER \u2500\u2500 *\/\nfunction processAndRender(data, isQC, response) {\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, data.tillStats || null, response);\n  updateReverseHitButtons(data, response);\n  document.getElementById('resultsSection').classList.add('visible');\n\n  \/\/ Show\/hide QC-specific elements\n  document.getElementById('operationalStatCards').style.display = isQC ? 'none' : 'flex';\n  document.getElementById('qcStatCards').style.display = isQC ? 'flex' : 'none';\n\n  renderTable();\n  renderPagination();\n\n  const hasData = currentData.length > 0;\n  document.getElementById('paginationFooter').style.display = hasData ? 'flex' : 'none';\n\n  \/\/ Show CSV Download button only when data is present\n  const csvBtn = document.getElementById('csvDownloadBtn');\n  if (hasData) {\n    csvBtn.classList.add('visible');\n  } else {\n    csvBtn.classList.remove('visible');\n  }\n}\n\n\/* \u2500\u2500 REVERSE HIT BUTTON VISIBILITY \u2500\u2500 *\/\nfunction isTruthyFlag(val) {\n  if (val === true || val === 1) return true;\n  if (typeof val === 'string') {\n    const s = val.trim().toLowerCase();\n    return s === 'true' || s === '1' || s === 'yes';\n  }\n  return false;\n}\n\nfunction getReverseHitFlag(sources, key) {\n  for (const src of sources) {\n    if (src && src[key] !== undefined && src[key] !== null) {\n      return isTruthyFlag(src[key]);\n    }\n  }\n  return false;\n}\n\nfunction updateReverseHitButtons(data, response) {\n  \/\/ PHP: accounts executives never see reverse hit buttons\n  if (isAccountsExecutiveUser()) {\n    hideReverseHitButtons();\n    return;\n  }\n\n  const stats = data?.stats || {};\n  const sources = [response, data, stats, response?.data?.stats];\n\n  let showProd = getReverseHitFlag(sources, 'show_reverse_productivity_hit_button');\n  let showSignOff = getReverseHitFlag(sources, 'show_reverse_signoff_hit_button');\n\n  \/\/ PHP list.blade.php parity fallback when explicit flags are absent\n  if (!showProd && !stats.is_reverse_productivity_hit) {\n    const prod = parseFloat(stats.productivity);\n    if (!Number.isNaN(prod) && prod >= 99.5) showProd = true;\n  }\n  if (!showSignOff && !stats.is_reversesign_off_hit) {\n    const qual = parseFloat(resolveQuality(stats, response));\n    if (!Number.isNaN(qual) && qual >= 99.5) showSignOff = true;\n  }\n\n  const prodBtn   = document.getElementById('reverseProdHitBtn');\n  const signBtn   = document.getElementById('reverseHitBtn');\n  const bannerRow = document.getElementById('reverseBannerRow');\n\n  prodBtn.classList.toggle('hidden', !showProd);\n  signBtn.classList.toggle('hidden', !showSignOff);\n  bannerRow.classList.toggle('hidden', !showProd && !showSignOff);\n}\n\nfunction hideReverseHitButtons() {\n  document.getElementById('reverseProdHitBtn').classList.add('hidden');\n  document.getElementById('reverseHitBtn').classList.add('hidden');\n  document.getElementById('reverseBannerRow').classList.add('hidden');\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\n\/** PHP list.blade.php: Quality % from API root `quality`; fallback stats \/ qaulity *\/\nfunction resolveQuality(stats, response) {\n  const candidates = [\n    response?.quality,\n    response?.qaulity,\n    stats?.quality,\n    stats?.qaulity,\n    stats?.quality_percentage,\n    response?.data?.quality,\n    response?.data?.qaulity,\n  ];\n  for (const val of candidates) {\n    if (val !== undefined && val !== null && val !== '') return fmt2(val);\n  }\n  return '100.00';\n}\n\n\/** PHP: >90% green (text-success), else red (text-danger) *\/\nfunction applyPctColor(elId, value) {\n  const el = document.getElementById(elId);\n  if (!el) return;\n  const n = parseFloat(value);\n  el.classList.remove('red', 'green');\n  if (!isNaN(n)) el.classList.add(n > 90 ? 'green' : 'red');\n}\n\nfunction updateStatCards(stats, isQC, tillStats, response) {\n  if (!stats) return;\n\n  const quality = resolveQuality(stats, response);\n\n  if (isQC) {\n    \/\/ Populate QC stat cards\n    document.getElementById('qc-sc-signoff').textContent     = fmt2(stats.signoff_hits);\n    document.getElementById('qc-sc-prod-hit').textContent    = fmt2(stats.productivity_hit);\n    document.getElementById('qc-sc-hrs-util').textContent    = fmt2(stats.hours_utilize);\n    document.getElementById('qc-sc-adhoc-util').textContent  = fmt2(stats.adhoc_utilize);\n    document.getElementById('qc-sc-hrs-total').textContent   = fmt2(stats.hours_total);\n    document.getElementById('qc-sc-prod-pct').textContent    = fmt2(stats.productivity) + '%';\n    document.getElementById('qc-sc-quality-pct').textContent = quality + '%';\n    applyPctColor('qc-sc-prod-pct', stats.productivity);\n    applyPctColor('qc-sc-quality-pct', quality);\n  } else {\n    \/\/ Populate Operational stat cards\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    document.getElementById('sc-quality-pct').textContent = quality + '%';\n    applyPctColor('sc-prod-pct', stats.productivity);\n    applyPctColor('sc-quality-pct', quality);\n  }\n\n  \/\/ Till Date section \u2014 shown only when the selected month is the current calendar month\n  const tillDateSection = document.getElementById('tillDateSection');\n  if (isCurrentMonthSelected() && tillStats) {\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(tillStats.signoff_hits);\n    document.getElementById('td-prod-hit').textContent   = fmt2(tillStats.productivity_hit);\n    document.getElementById('td-hrs-util').textContent   = fmt2(tillStats.hours_utilize);\n    document.getElementById('td-adhoc-util').textContent = fmt2(tillStats.adhoc_utilize);\n    document.getElementById('td-hrs-total').textContent  = fmt2(tillStats.hours_total);\n    document.getElementById('td-prod-pct').textContent   = fmt2(tillStats.productivity) + '%';\n    applyPctColor('td-prod-pct', tillStats.productivity);\n    tillDateSection.style.display = 'block';\n  } else {\n    tillDateSection.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 handleCsvDownload() {\n  if (!currentData || currentData.length === 0) {\n    alert('No data available to download.');\n    return;\n  }\n\n  const headers = [\n    'Assigned Date',\n    'Assigned To',\n    'Pesudo Name',\n    'Assigned DOS',\n    'DOS Completed',\n    'Pending DOS',\n    'Hrs Utilized',\n    'Ad Hoc Hrs',\n    'Productivity Hit',\n    'Sign Off Hit',\n    'Attendance'\n  ];\n\n  const escapeCSV = (val) => {\n    if (val === null || val === undefined) return '';\n    const str = String(val);\n    if (str.includes(',') || str.includes('\"') || str.includes('\\n')) {\n      return `\"${str.replace(\/\"\/g, '\"\"')}\"`;\n    }\n    return str;\n  };\n\n  const rows = currentData.map(row => {\n    const attendance = parseInt(row.attendance) === 1 ? 'Present' : 'Absent';\n    return [\n      escapeCSV(row.assigned_date || ''),\n      escapeCSV(row.assigned_to   || ''),\n      escapeCSV(row.pesudo_name   || ''),\n      escapeCSV(row.assigned_dos  ?? 0),\n      escapeCSV(row.complete_dos  ?? 0),\n      escapeCSV(row.pending_dos   ?? 0),\n      escapeCSV(fmt2(row.hours)),\n      escapeCSV(fmt2(row.adHours)),\n      escapeCSV(row.productivity_hit ?? 0),\n      escapeCSV(row.signoff_hit      ?? 0),\n      escapeCSV(attendance)\n    ].join(',');\n  });\n\n  const csvContent = [headers.join(','), ...rows].join('\\n');\n\n  \/\/ Build filename\n  const month = document.getElementById('selMonth');\n  const year  = document.getElementById('selYear').value;\n  const monthName = month.options[month.selectedIndex]?.text || month.value;\n  const tabLabel  = activeTab === 'qc' ? 'QC' : 'Operational';\n  const filename  = `Productivity_${tabLabel}_${monthName}_${year}.csv`;\n\n  const blob = new Blob([csvContent], { type: 'text\/csv;charset=utf-8;' });\n  const url  = URL.createObjectURL(blob);\n  const link = document.createElement('a');\n  link.href     = url;\n  link.download = filename;\n  document.body.appendChild(link);\n  link.click();\n  document.body.removeChild(link);\n  URL.revokeObjectURL(url);\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  document.getElementById('csvDownloadBtn').classList.remove('visible');\n  hideReverseHitButtons();\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  document.getElementById('csvDownloadBtn').classList.remove('visible');\n  hideReverseHitButtons();\n}\n<\/script>\n<\/div><\/div>\n\n<\/div><\/div><\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>Productivity List PRODUCTIVITY LIST Month* Select MonthJanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctoberNovemberDecember Year* Select Year Assign To* Select User Search CSV Download Operational QC Reverse Productivity Hit 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% Total Signoff Hits&#8230;<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_kad_post_transparent":"disable","_kad_post_title":"hide","_kad_post_layout":"fullwidth","_kad_post_sidebar_id":"","_kad_post_content_style":"unboxed","_kad_post_vertical_padding":"hide","_kad_post_feature":"hide","_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-v2.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages\/146","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ph-portal-v2.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/ph-portal-v2.zyneventures.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/ph-portal-v2.zyneventures.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/ph-portal-v2.zyneventures.com\/index.php\/wp-json\/wp\/v2\/comments?post=146"}],"version-history":[{"count":29,"href":"https:\/\/ph-portal-v2.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages\/146\/revisions"}],"predecessor-version":[{"id":1241,"href":"https:\/\/ph-portal-v2.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages\/146\/revisions\/1241"}],"wp:attachment":[{"href":"https:\/\/ph-portal-v2.zyneventures.com\/index.php\/wp-json\/wp\/v2\/media?parent=146"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}