« Back to History
daily_attendance_machineWise.php
|
20260721_154032.php
Initial Bulk Import
Copy Code
<?php /* ============================================================================= File: /erp/daily_attendance_machineWise.php Purpose: Full Month (1 to 31) Machine-Wise Matrix Attendance Entry with Card Headers, Excel Highlights & A4 Auto-Fit Printing ========================================================================== */ header('X-Frame-Options: SAMEORIGIN'); error_reporting(E_ALL); ini_set('display_errors', '1'); // Activity Helper Integration $require_activity_helper = __DIR__ . '/helpers/activity_helper.php'; if (file_exists($require_activity_helper)) { require_once $require_activity_helper; } // Authentication & ACL Context Pattern require __DIR__ . '/modules/auth/page_acl.php'; $ctx = page_require_access('manual_attendance'); $USER = $ctx['user']; $COMPANY_ID = (int)$ctx['company_id']; $USER_ID = (int)$USER['id']; $pdo = isset($ctx['pdo']) && $ctx['pdo'] instanceof PDO ? $ctx['pdo'] : null; if (!$pdo) { require __DIR__ . '/core/db.php'; } /* ============================================================================= AUTO TABLE SCHEMATICS CHECKER & CREATOR (For Dedicated Machine-Wise Grid) ========================================================================== */ try { $pdo->exec(" CREATE TABLE IF NOT EXISTS `employee_attendance_machinewise_grid` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `company_id` INT NOT NULL, `employee_id` INT NOT NULL, `attendance_date` DATE NOT NULL, `raw_input` VARCHAR(30) NOT NULL, `calculated_weight` DECIMAL(5,2) NOT NULL DEFAULT '0.00', `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, UNIQUE KEY `uk_comp_emp_date_machine` (`company_id`, `employee_id`, `attendance_date`), INDEX `idx_search_machine` (`company_id`, `attendance_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; "); } catch (Throwable $t) { // Fail-safe schema block } /* ===== Helpers Utility Tools ===== */ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function qv($k,$d=null){ return isset($_REQUEST[$k])? (is_array($_REQUEST[$k])?$_REQUEST[$k]:trim((string)$_REQUEST[$k])) : $d; } function pv($k,$d=null){ return isset($_POST[$k])? (is_array($_POST[$k])?$_POST[$k]:trim((string)$_POST[$k])) : $d; } function jexit($arr){ header('Content-Type: application/json; charset=utf-8'); echo json_encode($arr); exit; } /* ============================================================================= 2. INTERACTIVE AJAX PROCESSING GATEWAY ========================================================================== */ if (qv('ajax') === '1') { $act = qv('act',''); try { if ($act === 'departments') { $stm = $pdo->prepare(" SELECT department_name FROM company_employee_master WHERE company_id = ? AND is_active = 1 AND department_name <> '' GROUP BY department_name ORDER BY department_name ASC "); $stm->execute([$COMPANY_ID]); $items = $stm->fetchAll(PDO::FETCH_ASSOC); jexit(['ok' => true, 'items' => $items]); } if ($act === 'get_matrix_grid') { $month = qv('month'); $dept_name = trim(qv('dept_name')); if (empty($dept_name) || empty($month)) { throw new Exception("Operational Parameters Missing."); } [$year, $month_num] = array_map('intval', explode('-', $month)); $days_in_month = cal_days_in_month(CAL_GREGORIAN, $month_num, $year); $dates_array = []; for ($d = 1; $d <= $days_in_month; $d++) { $dates_array[] = sprintf('%02d-%02d-%04d', $d, $month_num, $year); } $emp_stmt = $pdo->prepare(" SELECT id, name, employee_code FROM company_employee_master WHERE company_id = ? AND department_name = ? AND is_active = 1 ORDER BY name ASC "); $emp_stmt->execute([$COMPANY_ID, $dept_name]); $employees = $emp_stmt->fetchAll(PDO::FETCH_ASSOC); $start_iso = sprintf('%04d-%02d-01', $year, $month_num); $end_iso = sprintf('%04d-%02d-%02d', $year, $month_num, $days_in_month); $attendance_matrix = []; if (!empty($employees)) { $att_stmt = $pdo->prepare(" SELECT employee_id, attendance_date, raw_input FROM employee_attendance_machinewise_grid WHERE company_id = ? AND attendance_date BETWEEN ? AND ? "); $att_stmt->execute([$COMPANY_ID, $start_iso, $end_iso]); $raw_att = $att_stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($raw_att as $row) { $date_formatted = date('d-m-Y', strtotime($row['attendance_date'])); $attendance_matrix[$row['employee_id']][$date_formatted] = $row['raw_input']; } } jexit([ 'ok' => true, 'dates' => $dates_array, 'employees' => $employees, 'matrix' => $attendance_matrix ]); } if ($act === 'save_batch_matrix') { $matrix_json = isset($_POST['matrix_json']) ? trim($_POST['matrix_json']) : ''; $dept_name = trim(pv('dept_name')); $month = pv('month'); $machine_factor = (float)pv('machine_factor', 0.25); if(empty($dept_name) || empty($month) || $matrix_json === '') { throw new Exception("Missing required core grid parameters."); } $matrix_payload = json_decode($matrix_json, true); if (!is_array($matrix_payload)) { throw new Exception("Matrix data parsing failure."); } [$year, $month_num] = array_map('intval', explode('-', $month)); $days_in_month = cal_days_in_month(CAL_GREGORIAN, $month_num, $year); $start_iso = sprintf('%04d-%02d-01', $year, $month_num); $end_iso = sprintf('%04d-%02d-%02d', $year, $month_num, $days_in_month); $pdo->beginTransaction(); $emp_list_stmt = $pdo->prepare("SELECT id FROM company_employee_master WHERE company_id = ? AND department_name = ? AND is_active = 1"); $emp_list_stmt->execute([$COMPANY_ID, $dept_name]); $scoped_db_emp_ids = $emp_list_stmt->fetchAll(PDO::FETCH_COLUMN); if (empty($scoped_db_emp_ids)) { $scoped_db_emp_ids = [0]; } $in_clause_placeholders = implode(',', array_fill(0, count($scoped_db_emp_ids), '?')); $del_query = " DELETE FROM employee_attendance_machinewise_grid WHERE company_id = ? AND attendance_date BETWEEN ? AND ? AND employee_id IN ($in_clause_placeholders) "; $del_params = array_merge([$COMPANY_ID, $start_iso, $end_iso], $scoped_db_emp_ids); $del = $pdo->prepare($del_query); $del->execute($del_params); $ins = $pdo->prepare(" INSERT INTO employee_attendance_machinewise_grid (company_id, employee_id, attendance_date, raw_input, calculated_weight, created_at, updated_at) VALUES (?, ?, ?, ?, ?, NOW(), NOW()) "); $insert_count = 0; foreach ($matrix_payload as $emp_id => $dates_obj) { $emp_id = (int)$emp_id; if (!in_array($emp_id, $scoped_db_emp_ids)) continue; foreach ($dates_obj as $date_str => $raw_val) { $raw_val = strtoupper(trim($raw_val)); if ($raw_val === '') { $raw_val = 'A'; } $calculated_weight = 0.00; if (is_numeric($raw_val)) { $calculated_weight = (float)$raw_val * $machine_factor; } else { if ($raw_val === 'P') { $calculated_weight = 1.00; } elseif ($raw_val === 'PP') { $calculated_weight = 2.00; } elseif ($raw_val === 'H') { $calculated_weight = 0.50; } else { $raw_val = 'A'; $calculated_weight = 0.00; } } $d = DateTime::createFromFormat('d-m-Y', $date_str); $db_date = $d ? $d->format('Y-m-d') : ''; if (!empty($db_date)) { $ins->execute([$COMPANY_ID, $emp_id, $db_date, $raw_val, $calculated_weight]); $insert_count++; } } } if (function_exists('log_activity')) { log_activity([ 'user_id' => $USER_ID, 'activity_type' => 'save_machinewise_grid_matrix', 'description' => "Saved full month machine attendance sheet for Dept: $dept_name, Month: $month. Affected Rows: $insert_count", 'reference_id' => $COMPANY_ID, 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '' ]); } $pdo->commit(); jexit([ 'ok' => true, 'inserted_rows' => $insert_count, 'msg' => 'Total ' . $insert_count . ' Machine-Wise data records successfully committed!' ]); } } catch (Throwable $e) { if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack(); jexit(['ok' => false, 'msg' => 'Database Error: ' . $e->getMessage()]); } } require_once __DIR__ . '/partials/header.php'; ?> <div class="container-fluid py-4 section-to-hide-print"> <div class="card shadow-sm border-0 mb-4 bg-white"> <div class="card-body py-3 d-flex justify-content-between align-items-center flex-wrap gap-2"> <div> <h4 class="fw-bold mb-0 text-dark">Live Machine-Wise Attendance Entry Matrix</h4> <p class="text-muted small mb-0">Directly input machine counts (e.g. <b>4, 5, 6, 8, 9, 10</b>) or codes (P, PP, A, H) with safe Excel freeze views.</p> </div> <div id="liveSyncBadge" class="badge bg-dark px-3 py-2 d-flex align-items-center font-monospace text-warning"> <i class="bi bi-cpu-fill me-1"></i> EXCEL CARD STICKY PANEL ACTIVE </div> </div> </div> <div class="card shadow-sm border-0 mb-4"> <div class="card-body bg-light rounded"> <div class="row g-3 align-items-end"> <div class="col-md-3"> <label class="form-label small fw-bold text-uppercase">Working Month</label> <input type="month" id="ctrl_month" class="form-control fw-bold" value="<?= date('Y-m') ?>" onchange="renderAttendanceMatrixGrid()"> </div> <div class="col-md-5"> <label class="form-label small fw-bold text-uppercase">Operational Department</label> <select id="ctrl_dept" class="form-select border-primary fw-bold" onchange="renderAttendanceMatrixGrid()"> <option value="">-- Choose Working Department Group --</option> </select> </div> <div class="col-md-4"> <label class="form-label small fw-bold text-primary text-uppercase">Per Machine Attendance Value</label> <div class="input-group"> <span class="input-group-text bg-primary text-white fw-bold">1 Machine =</span> <input type="number" id="ctrl_factor" class="form-control bg-white fw-bold text-center text-primary" step="0.01" min="0.01" max="1" value="0.25" oninput="recalculateLiveGridTotals()"> <span class="input-group-text bg-white small text-muted">Attendance Day</span> </div> </div> </div> </div> </div> <!-- Live Grid Mass Auto Fill Operations Panel with Dynamic Badges Container --> <div id="macroControlsCard" class="card border-primary mb-4 shadow-sm d-none position-sticky" style="top: 10px; z-index: 1000;"> <div class="card-header bg-primary text-white fw-bold py-2 d-flex justify-content-between align-items-center"> <span><i class="bi bi-lightning-charge-fill me-1"></i> Live Grid Mass Auto Fill Operations Panel</span> <span class="badge bg-white text-primary small">Hamesha Upar Visible</span> </div> <div class="card-body bg-white py-3"> <div id="card_sticky_employee_container" class="d-flex align-items-center gap-2 overflow-auto pb-3 mb-3 border-bottom style-scrollbar"> <!-- Loaded Dynamically via JavaScript API Streams --> </div> <div class="row g-3 align-items-center"> <div class="col-md-4"> <div class="input-group"> <span class="input-group-text bg-dark text-warning fw-bold small text-uppercase">Machine Count</span> <input type="number" id="macro_machine_val" class="form-control text-center fw-bold" placeholder="e.g. 4"> <button class="btn btn-warning fw-bold text-dark" type="button" onclick="executeMachineMacroFill()">Fill Machines</button> </div> </div> <div class="col-md-4"> <div class="input-group"> <span class="input-group-text bg-light fw-bold small text-uppercase">Holiday Target</span> <select id="macro_hol_date_select" class="form-select font-monospace"></select> <input type="text" id="macro_hol_status_val" class="form-control text-center fw-bold font-monospace" value="4" style="max-width: 70px;"> <button class="btn btn-info text-dark fw-bold" type="button" onclick="executeMacroHolidayFillDropdown()">Apply Day</button> </div> </div> <div class="col-md-2"> <div class="btn-group w-100 shadow-sm"> <button type="button" class="btn btn-success fw-bold text-white btn-sm" onclick="executeStandardMacroFill('4')">Fill 4</button> <button type="button" class="btn btn-dark fw-bold text-warning btn-sm" onclick="executeStandardMacroFill('8')">Fill 8</button> </div> </div> <div class="col-md-2 text-end"> <button type="button" class="btn btn-danger fw-bold w-100" onclick="clearEmptyGridBlocksFields()"> <i class="bi bi-eraser-fill"></i> CLEAR EMPTY </button> </div> </div> </div> </div> </div> <div class="container-fluid print-container"> <form id="attendanceBatchSaveForm" autocomplete="off" onsubmit="return false;"> <div class="card shadow-sm border-0 p-0 mb-4 card-print-override"> <div class="matrix-scroll-wrapper"> <table id="matrixTable" class="table table-bordered align-middle text-center mb-0 excel-grid-table"> <thead> <tr><th class="p-4 text-muted bg-white">Please select targeted parameters to load matrix sheet view.</th></tr> </thead> <tbody></tbody> </table> </div> </div> <div id="saveActionContainer" class="card shadow-sm border-0 mb-4 d-none section-to-hide-print"> <div class="card-body bg-light text-end py-3 d-flex justify-content-between align-items-center"> <div> <!-- PRINT SHEET BUTTON -> A4 AUTOMATIC FIT STYLE EXECUTION --> <button type="button" class="btn btn-dark btn-lg px-4 fw-bold shadow-sm" onclick="printAttendanceReportGrid();"> <i class="bi bi-printer-fill me-1"></i> PRINT SHEET (A4 FIT) </button> </div> <div> <span class="text-muted me-3 small font-monospace">* Colors: <b>4 (P) Green | 8 (PP) Blue | 5 Yellow | 6 Purple | 9 Cyan | 10 Red</b>.</span> <button type="button" id="saveMatrixBtn" class="btn btn-success btn-lg px-5 fw-bold shadow" onclick="commitEntireMatrixToServer()"> <i class="bi bi-cloud-arrow-up-fill me-1"></i> COMMIT & SAVE MACHINE SHEET </button> </div> </div> </div> </form> </div> <script> const el = id => document.getElementById(id); document.addEventListener('DOMContentLoaded', async () => { try { const res = await fetch('?ajax=1&act=departments').then(r=>r.json()); if(res.ok) { res.items.forEach(d => { el('ctrl_dept').add(new Option(d.department_name, d.department_name)); }); } } catch(err) { console.error(err); } }); let serverDatesRef = []; let serverEmployeesRef = []; async function renderAttendanceMatrixGrid() { const month = el('ctrl_month').value; const deptName = el('ctrl_dept').value; if(!deptName || !month) { el('macroControlsCard').classList.add('d-none'); el('saveActionContainer').classList.add('d-none'); el('matrixTable').innerHTML = '<thead><tr><th class="p-4 text-muted bg-white">Please select targeted parameters to load matrix sheet view.</th></tr></thead>'; return; } try { const res = await fetch(`?ajax=1&act=get_matrix_grid&month=${month}&dept_name=${encodeURIComponent(deptName)}`); const data = await res.json(); if(data.ok) { serverDatesRef = data.dates; serverEmployeesRef = data.employees; const stickyEmpContainer = el('card_sticky_employee_container'); stickyEmpContainer.innerHTML = `<span class="badge bg-secondary p-2 text-uppercase font-monospace small flex-shrink-0" style="font-size: 0.78rem;"><i class="bi bi-people-fill"></i> Column Order →</span>`; serverEmployeesRef.forEach((emp, index) => { stickyEmpContainer.innerHTML += ` <div class="sticky-emp-badge-item d-flex flex-column align-items-center text-center p-2 rounded border bg-light shadow-sm flex-shrink-0 style-badge-box" id="sticky_badge_emp_${emp.id}"> <span class="text-dark fw-bold text-truncate small" style="max-width: 130px; font-size: 0.85rem;">${index + 1}. ${emp.name}</span> <span class="badge bg-dark text-warning font-monospace py-0 px-1 mt-1" style="font-size:0.68rem;">CODE: ${emp.employee_code || 'N/A'}</span> </div>`; }); const holDateSel = el('macro_hol_date_select'); holDateSel.innerHTML = ''; serverDatesRef.forEach(dStr => { const dayNum = parseInt(dStr.split('-')[0]); holDateSel.add(new Option(`Day ${dayNum} (${dStr})`, dayNum)); }); const table = el('matrixTable'); table.innerHTML = ''; if(serverEmployeesRef.length === 0) { table.innerHTML = `<thead><tr><th class="p-4 text-danger fw-bold text-center bg-white">No active employee list found inside department specifications.</th></tr></thead>`; el('macroControlsCard').classList.add('d-none'); el('saveActionContainer').classList.add('d-none'); return; } el('macroControlsCard').classList.remove('d-none'); el('saveActionContainer').classList.remove('d-none'); let theadHtml = `<tr> <th class="freeze-date-col axis-head-junction">Date</th>`; serverEmployeesRef.forEach((emp, index) => { theadHtml += ` <th class="freeze-emp-header employee-col-header-${emp.id}" data-emp-id="${emp.id}"> <div class="form-check d-flex align-items-center justify-content-center gap-1 mb-1 border-bottom border-secondary pb-1 section-to-hide-print"> <input class="form-check-input emp-scope-checkbox cursor-pointer" type="checkbox" data-emp-id="${emp.id}" id="scope_check_${emp.id}" checked onchange="toggleSingleEmployeeColumnScope(${emp.id}, this.checked)"> <label class="form-check-label text-success fw-bold mb-0 cursor-pointer small font-monospace" for="scope_check_${emp.id}">Col ${index+1}</label> </div> <div class="emp-meta-wrapper text-truncate fw-bold text-warning">${emp.name}</div> </th>`; }); theadHtml += `</tr>`; table.innerHTML += `<thead>${theadHtml}</thead>`; let tbodyHtml = ''; serverDatesRef.forEach((dStr, rowIndex) => { tbodyHtml += `<tr data-row-idx="${rowIndex}"> <td class="freeze-date-col date-row-cell" id="date_cell_row_${rowIndex}" data-row-idx="${rowIndex}">${dStr}</td>`; serverEmployeesRef.forEach(emp => { const existingStatus = data.matrix[emp.id]?.[dStr] || ''; tbodyHtml += ` <td class="p-0 transition-all text-center align-middle grid-cell-interactive grid-column-emp-${emp.id}" id="cell_box_${emp.id}_${dStr}" data-row-idx="${rowIndex}" data-emp-id="${emp.id}" onmouseenter="setExcelHighlighting(${rowIndex}, ${emp.id})"> <input type="text" class="matrix-grid-input font-monospace text-center uppercase input-emp-id-${emp.id}" value="${existingStatus}" id="input_${emp.id}_${dStr}" data-emp-id="${emp.id}" data-date="${dStr}" data-row-idx="${rowIndex}" placeholder="A" onclick="this.select();" onfocus="setExcelHighlighting(${rowIndex}, ${emp.id})" onkeydown="handleGridKeystrokeNavigation(event, this)" oninput="evaluateInputStylesLive(this)"> </td>`; }); tbodyHtml += `</tr>`; }); let tfootHtml = `<tr class="summary-row-footer"> <td class="freeze-date-col bg-dark text-warning border-top-double">TOTAL DAYS</td>`; serverEmployeesRef.forEach(emp => { tfootHtml += ` <td class="p-2 font-monospace align-middle grid-column-emp-${emp.id} border-top-double text-center text-white" id="summary_panel_emp_${emp.id}" style="font-size:0.92rem; min-width: 140px;"> <div class="text-warning text-sum-total fw-bold"><span id="sum_pct_${emp.id}">0.00</span> Days</div> </td>`; }); tfootHtml += `</tr>`; table.innerHTML += `<tbody>${tbodyHtml}</tbody><tfoot>${tfootHtml}</tfoot>`; document.querySelectorAll('.matrix-grid-input').forEach(inputNode => { applyDynamicColorRules(inputNode); }); recalculateLiveGridTotals(); } } catch(err) { console.error(err); } } function setExcelHighlighting(rowIdx, empId) { document.querySelectorAll('.excel-highlight-row').forEach(cell => cell.classList.remove('excel-highlight-row')); document.querySelectorAll('.excel-highlight-col').forEach(cell => cell.classList.remove('excel-highlight-col')); document.querySelectorAll('.style-badge-box-highlight').forEach(badge => badge.classList.remove('style-badge-box-highlight')); document.querySelectorAll('.date-cell-highlight').forEach(cell => cell.classList.remove('date-cell-highlight')); document.querySelectorAll(`[data-row-idx="${rowIdx}"]`).forEach(cell => { cell.classList.add('excel-highlight-row'); }); const stickyDateCell = el(`date_cell_row_${rowIdx}`); if (stickyDateCell) { stickyDateCell.classList.add('date-cell-highlight'); } document.querySelectorAll(`.grid-column-emp-${empId}`).forEach(cell => { cell.classList.add('excel-highlight-col'); }); const activeBadge = el(`sticky_badge_emp_${empId}`); if (activeBadge) { activeBadge.classList.add('style-badge-box-highlight'); } } function handleGridKeystrokeNavigation(e, node) { const key = e.key; const empId = node.getAttribute('data-emp-id'); const currentDateStr = node.getAttribute('data-date'); const currentIndex = serverDatesRef.indexOf(currentDateStr); if (key === 'Enter') { e.preventDefault(); shiftFocusToNextRowCell(empId, currentIndex); } } function shiftFocusToNextRowCell(empId, currentIndex) { if (currentIndex !== -1 && currentIndex + 1 < serverDatesRef.length) { const nextDateStr = serverDatesRef[currentIndex + 1]; const nextInput = el(`input_${empId}_${nextDateStr}`); if (nextInput && !nextInput.disabled) { nextInput.focus(); nextInput.select(); } } } function toggleSingleEmployeeColumnScope(empId, isChecked) { const cells = document.querySelectorAll(`.grid-column-emp-${empId}`); cells.forEach(td => { const inp = td.querySelector('input'); if (isChecked) { td.style.opacity = '1'; if (inp) inp.disabled = false; } else { td.style.opacity = '0.3'; if (inp) inp.disabled = true; } }); } function evaluateInputStylesLive(inputNode) { applyDynamicColorRules(inputNode); recalculateLiveGridTotals(); } function applyDynamicColorRules(inputNode) { let rawStr = inputNode.value.toUpperCase().trim(); const parentTd = inputNode.parentElement; parentTd.className = parentTd.className.replace(/\bbg-cell-\S+/g, ''); if (rawStr === '') { parentTd.classList.add('bg-cell-a'); parentTd.style.setProperty('--cell-base-bg', '#ffffff'); return; } if (rawStr === '4' || rawStr === 'P') { parentTd.classList.add('bg-cell-4'); parentTd.style.setProperty('--cell-base-bg', '#e2f0d9'); } else if (rawStr === '8' || rawStr === 'PP') { parentTd.classList.add('bg-cell-8'); parentTd.style.setProperty('--cell-base-bg', '#ddebf7'); } else if (rawStr === '5') { parentTd.classList.add('bg-cell-5'); parentTd.style.setProperty('--cell-base-bg', '#fff2cc'); } else if (rawStr === '6') { parentTd.classList.add('bg-cell-6'); parentTd.style.setProperty('--cell-base-bg', '#e1d5e7'); } else if (rawStr === '9') { parentTd.classList.add('bg-cell-9'); parentTd.style.setProperty('--cell-base-bg', '#e5f5f9'); } else if (rawStr === '10') { parentTd.classList.add('bg-cell-10'); parentTd.style.setProperty('--cell-base-bg', '#fce4d6'); } else if (rawStr === 'H') { parentTd.classList.add('bg-cell-h'); parentTd.style.setProperty('--cell-base-bg', '#f2f4f4'); } else if (rawStr === 'A') { parentTd.classList.add('bg-cell-a'); parentTd.style.setProperty('--cell-base-bg', '#ffffff'); } else if (!isNaN(rawStr)) { parentTd.classList.add('bg-cell-generic-num'); parentTd.style.setProperty('--cell-base-bg', '#eaeded'); } else { inputNode.value = ''; parentTd.classList.add('bg-cell-a'); parentTd.style.setProperty('--cell-base-bg', '#ffffff'); } } function recalculateLiveGridTotals() { if (!serverEmployeesRef || serverEmployeesRef.length === 0) return; const factor = parseFloat(el('ctrl_factor').value) || 0.25; serverEmployeesRef.forEach(emp => { let totalWeight = 0.00; serverDatesRef.forEach(dStr => { const inp = el(`input_${emp.id}_${dStr}`); if (inp) { let v = inp.value.toUpperCase().trim(); if (v !== '') { if (!isNaN(v)) { totalWeight += (parseFloat(v) * factor); } else { if (v === 'P') totalWeight += 1.00; else if (v === 'PP') totalWeight += 2.00; else if (v === 'H') totalWeight += 0.50; } } } }); if (el(`sum_pct_${emp.id}`)) el(`sum_pct_${emp.id}`).innerText = totalWeight.toFixed(2); }); } function clearEmptyGridBlocksFields() { serverEmployeesRef.forEach(emp => { if(el(`scope_check_${emp.id}`).checked) { serverDatesRef.forEach(dStr => { const inp = el(`input_${emp.id}_${dStr}`); if(inp && !inp.value.trim()) { inp.value = 'A'; evaluateInputStylesLive(inp); } }); } }); } function executeStandardMacroFill(targetChar) { serverEmployeesRef.forEach(emp => { if(el(`scope_check_${emp.id}`).checked) { serverDatesRef.forEach(dStr => { const inputField = el(`input_${emp.id}_${dStr}`); if(inputField && !inputField.value.trim()) { inputField.value = targetChar; evaluateInputStylesLive(inputField); } }); } }); } function executeMachineMacroFill() { const machinePreset = el('macro_machine_val').value.trim(); if(!machinePreset) return; executeStandardMacroFill(machinePreset); } function executeMacroHolidayFillDropdown() { const targetDay = parseInt(el('macro_hol_date_select').value); const targetStatus = el('macro_hol_status_val').value.trim(); if(isNaN(targetDay) || !targetStatus) return; serverEmployeesRef.forEach(emp => { if(el(`scope_check_${emp.id}`).checked) { serverDatesRef.forEach(dStr => { if(parseInt(dStr.split('-')[0]) === targetDay) { const fullInput = el(`input_${emp.id}_${dStr}`); if(fullInput) { fullInput.value = targetStatus; evaluateInputStylesLive(fullInput); } } }); } }); } async function commitEntireMatrixToServer() { const btn = el('saveMatrixBtn'); const factor = parseFloat(el('ctrl_factor').value) || 0.25; btn.disabled = true; let matrixDataMap = {}; serverEmployeesRef.forEach(emp => { if(el(`scope_check_${emp.id}`).checked) { matrixDataMap[emp.id] = {}; serverDatesRef.forEach(dStr => { const inp = el(`input_${emp.id}_${dStr}`); let valToSend = inp ? inp.value.toUpperCase().trim() : 'A'; if(valToSend === '') valToSend = 'A'; matrixDataMap[emp.id][dStr] = valToSend; }); } }); const fd = new FormData(); fd.append('dept_name', el('ctrl_dept').value); fd.append('month', el('ctrl_month').value); fd.append('machine_factor', factor); fd.append('matrix_json', JSON.stringify(matrixDataMap)); try { const response = await fetch('?ajax=1&act=save_batch_matrix', { method: 'POST', body: fd }); const res = await response.json(); if(res.ok) { alert(res.msg); renderAttendanceMatrixGrid(); } else { alert("Error: " + res.msg); } } catch(e) { console.error(e); } btn.disabled = false; } /* ============================================================================= HIGHLY ADVANCED DYNAMIC A4 LANDSCAPE COMPLETE SINGLE PAGE AUTO-FIT PRINT ENGINE ========================================================================== */ function printAttendanceReportGrid() { const dept = el('ctrl_dept').value || 'N/A'; const month = el('ctrl_month').value || 'N/A'; let printWin = window.open('', '', 'width=1300,height=900'); // Exact mapping of colors & headers scale for print output let htmlContent = `<html><head><title>Machine Attendance Matrix Report - A4 Fit</title> <style> @page { size: A4 landscape; margin: 5mm; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 5mm; color: #000; background: #fff; } .print-header { margin-bottom: 8px; border-bottom: 2px solid #212529; padding-bottom: 4px; } .print-header h3 { margin: 0; font-size: 16px; font-weight: bold; color: #111; text-transform: uppercase; } .print-header p { margin: 2px 0 0 0; font-size: 11px; color: #555; font-weight: 600; } /* Master Table Fit Scaling Rules for A4 Single Page Constraint */ table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 10px; text-transform: uppercase; } th, td { border: 1px solid #111; padding: 3px 2px; text-align: center; font-weight: bold; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } th { background-color: #212529 !important; color: #fff !important; font-size: 9px; } .date-col { width: 65px; background-color: #f5f5f5 !important; font-size: 9.5px; border-right: 2px solid #111; } .total-row { background-color: #212529 !important; color: #fff !important; } .total-row td { border-top: 2px double #000 !important; font-size: 10px; } /* Retain Color Coding rules inside Printed Sheet Document */ .p-4 { background-color: #e2f0d9 !important; color: #385723 !important; } .p-8 { background-color: #ddebf7 !important; color: #1f4e78 !important; } .p-5 { background-color: #fff2cc !important; color: #7f6000 !important; } .p-6 { background-color: #e1d5e7 !important; color: #533263 !important; } .p-9 { background-color: #e5f5f9 !important; color: #166a70 !important; } .p-10 { background-color: #fce4d6 !important; color: #c65911 !important; } .p-a { background-color: #ffffff !important; color: #bdc3c7 !important; } </style></head> <body> <div class="print-header"> <h3>Machine Attendance Matrix Report</h3> <p>Department: <b>${dept}</b> | Target Month: <b>${month}</b> | Generated on: <b>${new Date().toLocaleDateString('en-GB')}</b></p> </div> <table> <thead> <tr> <th class="date-col">Date</th>`; // Add only active checked employee columns inside print document let validEmpIndices = []; serverEmployeesRef.forEach((emp, i) => { if(el(`scope_check_${emp.id}`).checked) { htmlContent += `<th>${emp.name}<br><span style="font-size:7.5px; opacity:0.85;">${emp.employee_code || ''}</span></th>`; validEmpIndices.push(emp.id); } }); htmlContent += `</tr></thead><tbody>`; // Body Matrix Data mapping stream loops serverDatesRef.forEach(dStr => { htmlContent += `<tr><td class="date-col">${dStr}</td>`; validEmpIndices.forEach(empId => { let val = el(`input_${empId}_${dStr}`).value.toUpperCase().trim() || 'A'; let cls = 'p-a'; if (val === '4' || val === 'P') cls = 'p-4'; else if (val === '8' || val === 'PP') cls = 'p-8'; else if (val === '5') cls = 'p-5'; else if (val === '6') cls = 'p-6'; else if (val === '9') cls = 'p-9'; else if (val === '10') cls = 'p-10'; htmlContent += `<td class="${cls}">${val}</td>`; }); htmlContent += `</tr>`; }); // Summary Calculations Bottom Print Row htmlContent += `<tr class="total-row"><td class="date-col" style="background-color:#212529 !important; color:#ffc107 !important;">TOTALS</td>`; validEmpIndices.forEach(empId => { let daysTot = el(`sum_pct_${empId}`).innerText; htmlContent += `<style>.total-row td{color:#ffc107 !important;}</style><td>${daysTot} D</td>`; }); htmlContent += `</tr></tbody></table></body></html>`; printWin.document.write(htmlContent); printWin.document.close(); setTimeout(() => { printWin.focus(); printWin.print(); printWin.close(); }, 500); } </script> <style> /* Precise Outer Matrix Shell Layout Rules */ .matrix-scroll-wrapper { max-height: 520px; max-width: 100%; overflow: auto !important; position: relative; background-color: #ffffff; } .excel-grid-table { border-collapse: separate !important; border-spacing: 0 !important; width: max-content; table-layout: fixed; } /* Freeze Matrix Headers at Top of table viewport wrapper */ .excel-grid-table thead th { position: sticky !important; top: 0 !important; background-color: #212529 !important; color: #ffffff !important; z-index: 10; min-width: 130px; max-width: 150px; padding: 12px 5px !important; border-bottom: 2px solid #000000 !important; border-top: 1px solid #343a40 !important; } /* Freeze Dates Axis on Left view boundary panel */ .excel-grid-table .freeze-date-col { position: sticky !important; left: 0 !important; background-color: #f8f9fa !important; color: #212529 !important; font-weight: bold; z-index: 8; min-width: 105px; max-width: 115px; border-right: 2px solid #343a40 !important; border-left: 1px solid #dee2e6 !important; box-shadow: 2px 0 4px rgba(0,0,0,0.08); } /* Corner Header Junction Block Mapping Overlay Layer */ .excel-grid-table thead th.axis-head-junction { position: sticky !important; top: 0 !important; left: 0 !important; background-color: #111418 !important; color: #ffc107 !important; z-index: 25 !important; font-weight: 900; } /* ADVANCED EXCEL MULTI-AXIS DYNAMIC HIGHLIGHT ENGINE RULES */ .excel-grid-table tbody tr td.grid-cell-interactive { background-color: var(--cell-base-bg, #ffffff); } /* Row Inner Cells Highlights */ .excel-grid-table tbody tr.excel-highlight-row td.grid-cell-interactive { background-image: linear-gradient(rgba(13, 110, 253, 0.12), rgba(13, 110, 253, 0.12)) !important; } /* CORE ROW FIXED: Explicit left-hand sticky Date TD cell Excel style dark highlights */ .excel-grid-table tbody tr td.freeze-date-col.date-cell-highlight { background-color: #ffc107 !important; color: #000000 !important; font-weight: 900 !important; } /* Column Highlight Blending */ .excel-grid-table tbody tr td.grid-cell-interactive.excel-highlight-col { background-image: linear-gradient(rgba(13, 110, 253, 0.08), rgba(13, 110, 253, 0.08)) !important; } /* Active interacting focal junction cell */ .grid-cell-interactive:focus-within { outline: 2px solid #000000 !important; background-image: none !important; z-index: 15 !important; } /* Horizontal Badges Scrollbar Formatting Rules */ .style-scrollbar::-webkit-scrollbar { height: 6px; } .style-scrollbar::-webkit-scrollbar-track { background: #f1f1f1; } .style-scrollbar::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; } .style-scrollbar::-webkit-scrollbar-thumb:hover { background: #555; } /* Sticky Badges Layout Styling */ .style-badge-box { transition: all 0.2s ease; min-width: 150px; border: 1px solid #dee2e6 !important; } .style-badge-box-highlight { background-color: #ffc107 !important; border-color: #ff9800 !important; transform: scale(1.05); box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important; } /* Grid Cell Dynamic Text Variables Mappings */ .bg-cell-4 { color: #385723 !important; } .bg-cell-8 { color: #1f4e78 !important; } .bg-cell-5 { color: #7f6000 !important; } .bg-cell-6 { color: #533263 !important; } .bg-cell-9 { color: #166a70 !important; } .bg-cell-10 { color: #c65911 !important; } .bg-cell-h { color: #7b7d7d !important; } .bg-cell-a { color: #bdc3c7 !important; } .bg-cell-generic-num { color: #1b4f72 !important; } .matrix-grid-input { width: 100%; height: 38px; border: 0 !important; background: transparent !important; font-weight: 900; outline: none !important; font-size: 1.1rem; color: inherit; } .border-top-double { border-top: 3px double #000 !important; } .summary-row-footer td { background-color: #212529 !important; color: #ffffff !important; } </style> <?php require_once __DIR__ . '/partials/footer.php'; ?>