{"id":138,"date":"2026-03-31T06:54:42","date_gmt":"2026-03-31T06:54:42","guid":{"rendered":"https:\/\/ph-portal.zyneventures.com\/?page_id=138"},"modified":"2026-03-31T06:58:34","modified_gmt":"2026-03-31T06:58:34","slug":"user-adjustment","status":"publish","type":"page","link":"https:\/\/ph-portal.zyneventures.com\/index.php\/user-adjustment\/","title":{"rendered":"User Adjustment"},"content":{"rendered":"<style>.kadence-column138_6767dd-40 > .kt-inside-inner-col,.kadence-column138_6767dd-40 > .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-column138_6767dd-40 > .kt-inside-inner-col{column-gap:var(--global-kb-gap-sm, 1rem);}.kadence-column138_6767dd-40 > .kt-inside-inner-col{flex-direction:column;}.kadence-column138_6767dd-40 > .kt-inside-inner-col > .aligncenter{width:100%;}.kadence-column138_6767dd-40 > .kt-inside-inner-col:before{opacity:0.3;}.kadence-column138_6767dd-40{position:relative;}@media all and (max-width: 1024px){.kadence-column138_6767dd-40 > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}@media all and (max-width: 767px){.kadence-column138_6767dd-40 > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}<\/style>\n<div class=\"wp-block-kadence-column kadence-column138_6767dd-40\"><div class=\"kt-inside-inner-col\"><style>.kb-row-layout-id138_6b3421-9d > .kt-row-column-wrap{align-content:start;}:where(.kb-row-layout-id138_6b3421-9d > .kt-row-column-wrap) > .wp-block-kadence-column{justify-content:start;}.kb-row-layout-id138_6b3421-9d > .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-id138_6b3421-9d > .kt-row-layout-overlay{opacity:0.30;}@media all and (max-width: 1024px){.kb-row-layout-id138_6b3421-9d > .kt-row-column-wrap{grid-template-columns:minmax(0, 1fr);}}@media all and (max-width: 767px){.kb-row-layout-id138_6b3421-9d > .kt-row-column-wrap{grid-template-columns:minmax(0, 1fr);}}<\/style><div class=\"kb-row-layout-wrap kb-row-layout-id138_6b3421-9d 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-column138_492b9f-2b > .kt-inside-inner-col,.kadence-column138_492b9f-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-column138_492b9f-2b > .kt-inside-inner-col{column-gap:var(--global-kb-gap-sm, 1rem);}.kadence-column138_492b9f-2b > .kt-inside-inner-col{flex-direction:column;}.kadence-column138_492b9f-2b > .kt-inside-inner-col > .aligncenter{width:100%;}.kadence-column138_492b9f-2b > .kt-inside-inner-col:before{opacity:0.3;}.kadence-column138_492b9f-2b{position:relative;}@media all and (max-width: 1024px){.kadence-column138_492b9f-2b > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}@media all and (max-width: 767px){.kadence-column138_492b9f-2b > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}<\/style>\n<div class=\"wp-block-kadence-column kadence-column138_492b9f-2b\"><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            --error-red: #D93025;\n        }\n\n        body { \n            font-family: 'Sora', sans-serif; \n            color: var(--text-dark); \n        }\n\n        h2 { \n            font-size: 21px; \n            font-weight: 500; \n            text-transform: uppercase; \n            letter-spacing: 0.5px; \n            margin-bottom: 30px;\n        }\n\n        .filter-container {\n            display: grid;\n            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n            gap: 20px;\n            align-items: end;\n            margin-bottom: 25px;\n        }\n\n        .filter-group {\n            display: flex;\n            flex-direction: column;\n            gap: 8px;\n        }\n\n        .filter-group label {\n            font-size: 14px;\n            font-weight: 400;\n        }\n\n        .form-control {\n            padding: 8px 15px;\n            border: 1px solid #8D8C9C;\n            border-radius: 6px;\n            font-family: 'Sora';\n            font-size: 12px;\n            font-weight: 400;\n            outline: none;\n            height: 42px;\n            box-sizing: border-box;\n        }\n\n        .btn-row {\n            display: flex;\n            gap: 15px;\n            margin-bottom: 40px;\n            flex-wrap: wrap;\n        }\n\n        .btn {\n            padding: 8px 24px;\n            border-radius: 6px;\n            font-size: 12px;\n            font-weight: 500;\n            cursor: pointer;\n            border: none;\n            transition: 0.2s;\n            height: 42px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .btn-primary { background: var(--primary-blue); color: white; min-width: 140px; }\n        .btn-primary:hover { background: var(--hover-blue); }\n        .btn-clear { background: #EBF2FF; color: var(--primary-blue); min-width: 120px; }\n        .btn-secondary { background: var(--primary-blue); color: white; width: auto; }\n\n        .table-wrapper {\n            width: 100%;\n            overflow-x: auto;\n            border-radius: 6px;\n            border: 1px solid var(--border-color);\n        }\n\n        table {\n            width: 100%;\n            border-collapse: collapse;\n            text-align: left;\n            min-width: 1000px;\n        }\n\n        thead tr { background-color: #F5F5F5; }\n\n        th {\n            padding: 12px 15px;\n            font-size: 12px;\n            font-weight: 500;\n            color: #212529;\n            border-bottom: 1px solid var(--border-color);\n        }\n\n        td {\n            padding: 18px 20px;\n            font-size: 12px;\n            color: #8D8C9C;\n            border-bottom: 1px solid var(--border-color);\n        }\n\n        .action-link {\n            color: var(--error-red);\n            text-decoration: none;\n            cursor: pointer;\n            font-weight: 400;\n        }\n\n        \/* \u2500\u2500 Pagination (Project.html style with ellipsis) \u2500\u2500 *\/\n        .pagination-footer { \n            margin-top: 50px; \n            display: flex; \n            justify-content: space-between; \n            align-items: center; \n            color: var(--text-muted); \n            font-size: 12px;\n            flex-wrap: wrap;\n            gap: 12px;\n        }\n        .pagination-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n        .page-num { \n            cursor: pointer; \n            width: 35px; \n            height: 35px; \n            display: flex; \n            align-items: center; \n            justify-content: center; \n            border-radius: 50%; \n            font-weight: 400;\n            border: 1px solid transparent;\n            font-size: 12px;\n            flex-shrink: 0;\n        }\n        .page-num.active { background: var(--primary-blue); color: white; }\n        .page-num:not(.active):hover { background: #f1f3f4; }\n        .nav-btn { \n            cursor: pointer; \n            color: var(--text-dark); \n            font-weight: 400;\n            white-space: nowrap;\n            font-size: 12px;\n        }\n        .nav-btn.disabled { color: #ccc; pointer-events: none; }\n\n        \/* \u2500\u2500 Responsive \u2500\u2500 *\/\n        @media (max-width: 1024px) {\n            body { padding-left: 40px; }\n            .filter-container { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }\n        }\n\n        @media (max-width: 768px) {\n            body { padding-left: 20px; padding-right: 20px; }\n            .filter-container { grid-template-columns: 1fr 1fr; }\n            .btn-row { gap: 10px; }\n            .pagination-footer { flex-direction: column; align-items: flex-start; gap: 16px; }\n            .pagination-controls { gap: 6px; }\n            h2 { font-size: 17px; margin-bottom: 20px; }\n        }\n\n        @media (max-width: 480px) {\n            body { padding-left: 20px; padding-right: 16px; }\n            .filter-container { grid-template-columns: 1fr; }\n            .btn-row { flex-direction: column; }\n            .btn { width: 100%; }\n            .page-num { width: 30px; height: 30px; font-size: 11px; }\n        }\n    <\/style>\n\n\n    <h2>User Adjustments<\/h2><br>\n\n    <div class=\"filter-container\">\n        <div class=\"filter-group\">\n            <label>Assigned to<\/label>\n            <select id=\"assignedToFilter\" class=\"form-control\">\n                <option value=\"\"> <\/option>\n            <\/select>\n        <\/div>\n        <div class=\"filter-group\">\n            <label>Month<\/label>\n            <select id=\"monthFilter\" class=\"form-control\">\n                <option value=\"\">Select Month<\/option>\n                <option value=\"1\">January<\/option>\n                <option value=\"2\">February<\/option>\n                <option value=\"3\">March<\/option>\n                <option value=\"4\">April<\/option>\n                <option value=\"5\">May<\/option>\n                <option value=\"6\">June<\/option>\n                <option value=\"7\">July<\/option>\n                <option value=\"8\">August<\/option>\n                <option value=\"9\">September<\/option>\n                <option value=\"10\">October<\/option>\n                <option value=\"11\">November<\/option>\n                <option value=\"12\">December<\/option>\n            <\/select>\n        <\/div>\n        <div class=\"filter-group\">\n            <label>Year<\/label>\n            <select id=\"yearFilter\" class=\"form-control\">\n                <option value=\"\">Select Year<\/option>\n                <option value=\"2024\">2024<\/option>\n                <option value=\"2025\">2025<\/option>\n                <option value=\"2026\">2026<\/option>\n            <\/select>\n        <\/div>\n        <button class=\"btn btn-clear\" onclick=\"clearFilters()\">Clear<\/button>\n        <button class=\"btn btn-primary\" onclick=\"applyFilters()\">Filter<\/button>\n    <\/div>\n\n    <div class=\"btn-row\">\n        <button class=\"btn btn-secondary\" onclick=\"openAddAdjustment()\">+ Add User Adjustment<\/button>\n        <button class=\"btn btn-secondary\" onclick=\"downloadCSV()\">Download CSV<\/button>\n    <\/div>\n\n    <div class=\"table-wrapper\">\n        <table>\n            <thead>\n                <tr>\n                    <th>User<\/th>\n                    <th>Description<\/th>\n                    <th>Type<\/th>\n                    <th>Hours<\/th>\n                    <th>Month<\/th>\n                    <th>Year<\/th>\n                    <th>Created By<\/th>\n                    <th>Action<\/th>\n                <\/tr>\n            <\/thead>\n            <tbody id=\"adjustmentTableBody\">\n                <tr><td colspan=\"8\" style=\"text-align:center;\">Loading data&#8230;<\/td><\/tr>\n            <\/tbody>\n        <\/table>\n    <\/div>\n\n    <div class=\"pagination-footer\">\n        <div class=\"pagination-controls\">\n            <span id=\"prevBtn\" class=\"nav-btn\">\u00ab Previous<\/span>\n            <div id=\"pageNumbers\" style=\"display:flex; gap:6px; flex-wrap:wrap;\"><\/div>\n            <span id=\"nextBtn\" class=\"nav-btn\">Next \u00bb<\/span>\n        <\/div>\n        <div id=\"entryInfo\">Showing 0 to 0 of 0 entries<\/div>\n    <\/div>\n\n    <script>\n        \/* CONFIGURATION *\/\n        const BASE_URL = 'https:\/\/api-ph-portal.zyneventures.com\/api\/v1';\n        const GET_ADJUSTMENTS_ENDPOINT = `${BASE_URL}\/schedule\/adjust-user-hours`;\n        const GET_USERS_ENDPOINT = `${BASE_URL}\/user\/table\/list`;\n        const DELETE_ADJUSTMENT_ENDPOINT = `${BASE_URL}\/schedule\/adjust-user-hours\/delete\/`;\n        const ADD_ADJUSTMENT_URL = 'https:\/\/ph-portal.zyneventures.com\/index.php\/adjust-user-hours\/';\n\n        const TOKEN = localStorage.getItem(\"TOKEN\");\n        \n        const getHeaders = () => ({\n            'Content-Type': 'application\/json',\n            'Authorization': `Bearer ${TOKEN}`\n        });\n\n        const extractListData = (payload) => {\n            if (Array.isArray(payload?.data)) return payload.data;\n            if (Array.isArray(payload?.data?.data)) return payload.data.data;\n            if (Array.isArray(payload?.data?.rows)) return payload.data.rows;\n            if (Array.isArray(payload?.rows)) return payload.rows;\n            return [];\n        };\n\n        const extractPaginationInfo = (payload, fallbackLimit) => {\n            const directPages = Number(\n                payload?.pagination?.pages ||\n                payload?.data?.pagination?.pages ||\n                payload?.last_page ||\n                payload?.data?.last_page ||\n                0\n            );\n\n            const limit = Number(\n                payload?.pagination?.limit ||\n                payload?.data?.pagination?.limit ||\n                payload?.per_page ||\n                payload?.data?.per_page ||\n                payload?.data?.perPage ||\n                fallbackLimit\n            );\n\n            if (Number.isFinite(directPages) && directPages > 0) {\n                return {\n                    pages: directPages,\n                    limit: Number.isFinite(limit) && limit > 0 ? limit : fallbackLimit\n                };\n            }\n\n            const total = Number(\n                payload?.pagination?.total ||\n                payload?.data?.pagination?.total ||\n                payload?.total ||\n                payload?.data?.total ||\n                0\n            );\n\n            const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : fallbackLimit;\n            const derivedPages = total > 0 ? Math.ceil(total \/ safeLimit) : 1;\n\n            return { pages: Math.max(1, derivedPages), limit: safeLimit, total };\n        };\n\n        function renderStateRow(message, color = 'inherit') {\n            document.getElementById('adjustmentTableBody').innerHTML =\n                `<tr><td colspan=\"8\" style=\"text-align:center; color:${color};\">${message}<\/td><\/tr>`;\n        }\n\n        function safeText(value, fallback = 'N\/A') {\n            if (value === null || value === undefined || value === '') return fallback;\n            return String(value)\n                .replace(\/&\/g, '&amp;')\n                .replace(\/<\/g, '&lt;')\n                .replace(\/>\/g, '&gt;')\n                .replace(\/\"\/g, '&quot;')\n                .replace(\/'\/g, '&#39;');\n        }\n\n        function normalizeMonthDisplay(monthValue) {\n            const months = [\"\", \"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\n            const rawValue = String(monthValue || '').trim();\n            const numericMonth = Number(rawValue);\n\n            if (Number.isFinite(numericMonth) && numericMonth >= 1 && numericMonth <= 12) {\n                return months[numericMonth];\n            }\n\n            return rawValue || 'N\/A';\n        }\n\n        async function fetchAllUsers() {\n            const defaultLimit = 500;\n            const buildUrl = (page, limit = defaultLimit) => {\n                const url = new URL(GET_USERS_ENDPOINT);\n                url.searchParams.set('page', String(page));\n                url.searchParams.set('limit', String(limit));\n                return url.toString();\n            };\n\n            const firstResponse = await fetch(buildUrl(1, defaultLimit), { headers: getHeaders() });\n            const firstResult = await firstResponse.json();\n\n            let users = extractListData(firstResult);\n            const { pages, limit } = extractPaginationInfo(firstResult, defaultLimit);\n\n            if (pages > 1) {\n                const requests = [];\n                for (let page = 2; page <= pages; page++) {\n                    requests.push(fetch(buildUrl(page, limit), { headers: getHeaders() }).then(res => res.json()));\n                }\n\n                const pageResults = await Promise.all(requests);\n                pageResults.forEach(result => {\n                    users = users.concat(extractListData(result));\n                });\n            }\n\n            return users;\n        }\n\n        let adjustmentData = [];\n        let currentPage = 1;\n        let totalPages = 1;\n        let totalEntries = 0;\n        const rowsPerPage = 10;\n\n        window.onload = () => {\n            if (!TOKEN) {\n                alert(\"No authorization token found. Please log in.\");\n                return;\n            }\n            renderStateRow('Loading data...');\n            fetchUsersDropdown();\n            fetchAdjustments();\n        };\n\n        async function fetchUsersDropdown() {\n            try {\n                const users = await fetchAllUsers();\n                const dropdown = document.getElementById('assignedToFilter');\n                dropdown.innerHTML = '<option value=\"\">Select User<\/option>';\n\n                users.forEach(user => {\n                    const option = document.createElement('option');\n                    option.value = user.id;\n                    option.textContent = user.name || user.user_name || `User ${user.id}`;\n                    dropdown.appendChild(option);\n                });\n            } catch (error) {\n                console.error(\"Error fetching users:\", error);\n            }\n        }\n\n        async function fetchAdjustments() {\n            const user_id = document.getElementById('assignedToFilter').value;\n            const month = document.getElementById('monthFilter').value;\n            const year = document.getElementById('yearFilter').value;\n\n            let queryParams = `?page=${currentPage}&limit=${rowsPerPage}`;\n            if (user_id) queryParams += `&user_id=${user_id}`;\n            if (month) queryParams += `&month=${month}`;\n            if (year) queryParams += `&year=${year}`;\n\n            try {\n                renderStateRow('Loading data...');\n                const response = await fetch(`${GET_ADJUSTMENTS_ENDPOINT}${queryParams}`, { headers: getHeaders() });\n                const result = await response.json();\n\n                if (response.ok && (result.status === \"success\" || result.code === 200 || result.data)) {\n                    adjustmentData = extractListData(result);\n                    const pagination = extractPaginationInfo(result, rowsPerPage);\n                    totalEntries = Number(\n                        result?.data?.pagination?.total ||\n                        result?.pagination?.total ||\n                        adjustmentData.length ||\n                        0\n                    );\n                    totalPages = pagination.pages || 1;\n                    renderTable();\n                } else {\n                    adjustmentData = [];\n                    totalEntries = 0;\n                    totalPages = 1;\n                    renderStateRow('No data found.');\n                    renderPagination();\n                }\n            } catch (error) {\n                console.error(\"Error fetching adjustments:\", error);\n                adjustmentData = [];\n                totalEntries = 0;\n                totalPages = 1;\n                renderStateRow('Failed to load data. Please check your connection.', 'red');\n                renderPagination();\n            }\n        }\n\n        \/* DELETE \u2014 GET \/schedule\/adjust-user-hours\/delete\/:id *\/\n        async function deleteEntry(id) {\n            if (confirm(\"Are you sure you want to delete this entry?\")) {\n                try {\n                    const response = await fetch(`${DELETE_ADJUSTMENT_ENDPOINT}${id}`, {\n                        method: 'GET',\n                        headers: getHeaders()\n                    });\n                    const result = await response.json();\n                    if (response.ok && (result.status === \"success\" || result.code === 200)) {\n                        fetchAdjustments();\n                    } else {\n                        alert(result.message || \"Failed to delete entry.\");\n                    }\n                } catch (error) {\n                    console.error(\"Error deleting entry:\", error);\n                    alert(\"An error occurred while deleting. Please try again.\");\n                }\n            }\n        }\n\n        function renderTable() {\n            const tbody = document.getElementById('adjustmentTableBody');\n            tbody.innerHTML = '';\n\n            if (adjustmentData.length === 0) {\n                renderStateRow('No records found.');\n                renderPagination();\n                return;\n            }\n\n            adjustmentData.forEach((item) => {\n                const tr = document.createElement('tr');\n                tr.innerHTML = `\n                    <td>${safeText(item.user_name)}<\/td>\n                    <td>${safeText(item.description, '')}<\/td>\n                    <td>${safeText(item.type, '')}<\/td>\n                    <td>${safeText(item.hours, '0')}<\/td>\n                    <td>${safeText(normalizeMonthDisplay(item.month))}<\/td>\n                    <td>${safeText(item.year)}<\/td>\n                    <td>${safeText(item.created_by_name)}<\/td>\n                    <td><a class=\"action-link\" onclick=\"deleteEntry(${item.id})\">Delete<\/a><\/td>\n                `;\n                tbody.appendChild(tr);\n            });\n\n            renderPagination();\n        }\n\n        \/* \u2500\u2500 Ellipsis pagination logic (Project.html pattern) \u2500\u2500 *\/\n        function buildPageList(current, total) {\n            if (total <= 5) return Array.from({ length: total }, (_, i) => i + 1);\n            const pages = [];\n            if (current <= 3) {\n                pages.push(1, 2, 3, 4, '...', total);\n            } else if (current >= total - 2) {\n                pages.push(1, '...', total - 3, total - 2, total - 1, total);\n            } else {\n                pages.push(1, '...', current - 1, current, current + 1, '...', total);\n            }\n            return pages;\n        }\n\n        function renderPagination() {\n            const startEntry = totalEntries === 0 ? 0 : ((currentPage - 1) * rowsPerPage) + 1;\n            const endEntry = Math.min(currentPage * rowsPerPage, totalEntries);\n            document.getElementById('entryInfo').textContent = `Showing ${startEntry} to ${endEntry} of ${totalEntries} entries`;\n\n            const container = document.getElementById('pageNumbers');\n            container.innerHTML = '';\n\n            const pageList = buildPageList(currentPage, totalPages);\n            pageList.forEach(p => {\n                const span = document.createElement('span');\n                if (p === '...') {\n                    span.className = 'page-num';\n                    span.textContent = '\u2026';\n                    span.style.cursor = 'default';\n                } else {\n                    span.className = p === currentPage ? 'page-num active' : 'page-num';\n                    span.textContent = p;\n                    span.onclick = () => { currentPage = p; fetchAdjustments(); };\n                }\n                container.appendChild(span);\n            });\n\n            const prevBtn = document.getElementById('prevBtn');\n            const nextBtn = document.getElementById('nextBtn');\n\n            prevBtn.className = currentPage === 1 ? 'nav-btn disabled' : 'nav-btn';\n            prevBtn.onclick = () => { if (currentPage > 1) { currentPage--; fetchAdjustments(); } };\n\n            nextBtn.className = (currentPage === totalPages || totalPages === 0) ? 'nav-btn disabled' : 'nav-btn';\n            nextBtn.onclick = () => { if (currentPage < totalPages) { currentPage++; fetchAdjustments(); } };\n        }\n\n        function applyFilters() {\n            currentPage = 1;\n            fetchAdjustments();\n        }\n\n        function clearFilters() { \n            document.getElementById('assignedToFilter').value = '';\n            document.getElementById('monthFilter').value = '';\n            document.getElementById('yearFilter').value = '';\n            currentPage = 1;\n            fetchAdjustments();\n        }\n\n        function openAddAdjustment() { window.location.href = ADD_ADJUSTMENT_URL; }\n\n        async function downloadCSV() {\n            const user_id = document.getElementById('assignedToFilter').value;\n            const month   = document.getElementById('monthFilter').value;\n            const year    = document.getElementById('yearFilter').value;\n\n            const buildQuery = (page) => {\n                let q = `?page=${page}&limit=500`;\n                if (user_id) q += `&user_id=${user_id}`;\n                if (month)   q += `&month=${month}`;\n                if (year)    q += `&year=${year}`;\n                return q;\n            };\n\n            try {\n                \/* \u2500\u2500 Fetch page 1 to get total pages \u2500\u2500 *\/\n                const firstRes    = await fetch(`${GET_ADJUSTMENTS_ENDPOINT}${buildQuery(1)}`, { headers: getHeaders() });\n                const firstResult = await firstRes.json();\n\n                let allData = extractListData(firstResult);\n                const { pages } = extractPaginationInfo(firstResult, 500);\n\n                \/* \u2500\u2500 Fetch remaining pages in parallel \u2500\u2500 *\/\n                if (pages > 1) {\n                    const reqs = [];\n                    for (let p = 2; p <= pages; p++) {\n                        reqs.push(\n                            fetch(`${GET_ADJUSTMENTS_ENDPOINT}${buildQuery(p)}`, { headers: getHeaders() })\n                                .then(r => r.json())\n                        );\n                    }\n                    const results = await Promise.all(reqs);\n                    results.forEach(r => { allData = allData.concat(extractListData(r)); });\n                }\n\n                if (!allData.length) {\n                    alert(\"No data available to download.\");\n                    return;\n                }\n\n                \/* \u2500\u2500 Build CSV \u2500\u2500 *\/\n                const csvSafe = (val) => {\n                    if (val === null || val === undefined) return '';\n                    const str = String(val).replace(\/\"\/g, '\"\"');\n                    return str.includes(',') || str.includes('\"') || str.includes('\\n') ? `\"${str}\"` : str;\n                };\n\n                const headers = ['User', 'Description', 'Type', 'Hours', 'Month', 'Year', 'Created By'];\n                const rows = allData.map(item => [\n                    csvSafe(item.user_name),\n                    csvSafe(item.description),\n                    csvSafe(item.type),\n                    csvSafe(item.hours ?? 0),\n                    csvSafe(normalizeMonthDisplay(item.month)),\n                    csvSafe(item.year),\n                    csvSafe(item.created_by_name)\n                ].join(','));\n\n                const csvContent = [headers.join(','), ...rows].join('\\n');\n                const blob = new Blob([csvContent], { type: 'text\/csv;charset=utf-8;' });\n                const url  = URL.createObjectURL(blob);\n                const a    = document.createElement('a');\n                a.href     = url;\n                a.download = `user_adjustments${year ? '_' + year : ''}${month ? '_m' + month : ''}.csv`;\n                document.body.appendChild(a);\n                a.click();\n                document.body.removeChild(a);\n                URL.revokeObjectURL(url);\n\n            } catch (error) {\n                console.error(\"CSV download error:\", error);\n                alert(\"Failed to download CSV. Please try again.\");\n            }\n        }\n    <\/script>\n<\/div><\/div>\n\n<\/div><\/div><\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>User Adjustments Assigned to Month Select MonthJanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctoberNovemberDecember Year Select Year202420252026 Clear Filter + Add User Adjustment Download CSV User Description Type Hours Month Year Created By Action Loading data&#8230; \u00ab Previous Next \u00bb Showing 0 to 0 of 0 entries<\/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-138","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages\/138","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=138"}],"version-history":[{"count":4,"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages\/138\/revisions"}],"predecessor-version":[{"id":145,"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/pages\/138\/revisions\/145"}],"wp:attachment":[{"href":"https:\/\/ph-portal.zyneventures.com\/index.php\/wp-json\/wp\/v2\/media?parent=138"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}