« Back to History
attendance_entry.php
|
20260722_120325.php
Initial Domain Snapshot
Copy Code
<?php /* ============================================================================= File: /erp/attendance_entry.php Purpose: Attendance UI (uses company_employee_master as employee source) Notes: - Logic/AJAX same as previous implementation. - Styling is in main.css (global). If header.php doesn't load page CSS, this file injects main + page CSS into <head> via JS (safe fallback). ============================================================================= */ error_reporting(E_ALL); ini_set('display_errors', 1); /* ---------- 1) AUTH & PDO (scoped) ---------- */ require __DIR__ . '/modules/auth/auth.php'; require_login(); $u = auth_user(); $company_id = (int)$u['company_id']; $user_id = (int)$u['id']; require_once __DIR__ . '/helpers/activity_helper.php'; $pdo = $GLOBALS['pdo'] ?? null; if (!$pdo) { require __DIR__ . '/core/db.php'; } /* ---------- 2) HELPERS ---------- */ function pv($k,$d=null){ return isset($_POST[$k]) ? trim((string)$_POST[$k]) : $d; } function gv($k,$d=null){ return isset($_GET[$k]) ? trim((string)$_GET[$k]) : $d; } function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } /* ---------- 3) Company name (header) ---------- */ $company_name = '(Company)'; try{ $st = $pdo->prepare('SELECT name FROM companies WHERE id=?'); $st->execute([$company_id]); $company_name = $st->fetchColumn() ?: $company_name; }catch(Throwable $e){} /* ---------- 4) Ensure local tables (safe create-if-not-exists) ---------- */ try{ $pdo->exec("CREATE TABLE IF NOT EXISTS attendance_days ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, employee_id BIGINT NOT NULL, adate DATE NOT NULL, attendance DECIMAL(5,2) NOT NULL DEFAULT 1.00, note VARCHAR(191) NULL, UNIQUE KEY uniq_emp_day (company_id, employee_id, adate) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); $pdo->exec("CREATE TABLE IF NOT EXISTS attendance_assignments ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, employee_id BIGINT NOT NULL, adate DATE NOT NULL, machine_no VARCHAR(20) NULL, machine_type VARCHAR(30) NULL, UNIQUE KEY uniq_assign (company_id, employee_id, adate) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); }catch(Throwable $e){ /* ignore create errors */ } /* ---------- 5) QUICK AJAX (autosave) ---------- */ if($_SERVER['REQUEST_METHOD']==='POST' && pv('action')==='quick'){ header('Content-Type: application/json'); try{ $kind = pv('kind'); // 'att' | 'att_val' | 'mach' $eid = (int)pv('eid',0); $adate = pv('date', date('Y-m-d')); if($eid<=0) throw new Exception('invalid employee'); if($kind==='att'){ $present = (int)pv('present',1); $val = $present ? 1.0 : 0.0; $st=$pdo->prepare("INSERT INTO attendance_days (company_id,employee_id,adate,attendance) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE attendance=VALUES(attendance)"); $st->execute([$company_id,$eid,$adate,$val]); if($val==0){ $pdo->prepare("DELETE FROM attendance_assignments WHERE company_id=? AND employee_id=? AND adate=?") ->execute([$company_id,$eid,$adate]); } $u = $ctx['user']; // Required by activity helper log_activity([ 'company_id' => $company_id, 'user_id' => $user_id, 'module' => 'attendance', 'page' => basename(__FILE__), 'action' => $val > 0 ? 'CREATE' : 'DELETE', 'entity_type' => 'attendance', 'entity_id' => $eid, 'description' => "Attendance for Employee ID {$eid} set to " . ($val > 0 ? 'Present' : 'Absent') . " on {$adate}" ]); echo json_encode(['ok'=>true,'saved'=>'att','val'=>$val]); exit; } if($kind==='att_val'){ $val = (float)pv('val',1); $st=$pdo->prepare("INSERT INTO attendance_days (company_id,employee_id,adate,attendance) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE attendance=VALUES(attendance)"); $st->execute([$company_id,$eid,$adate,$val]); $u = $ctx['user']; // Required by activity helper log_activity([ 'company_id' => $company_id, 'user_id' => $user_id, 'module' => 'attendance', 'page' => basename(__FILE__), 'action' => 'UPDATE', 'entity_type' => 'attendance', 'entity_id' => $eid, 'description' => "Attendance value for Employee ID {$eid} updated to {$val} on {$adate}" ]); echo json_encode(['ok'=>true,'saved'=>'att_val','val'=>$val]); exit; } if($kind==='mach'){ $mno = pv('machine_no',null); $mtype = pv('mtype',null); // 'Loom' | 'Rapier' $st=$pdo->prepare("INSERT INTO attendance_assignments (company_id,employee_id,adate,machine_no,machine_type) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE machine_no=VALUES(machine_no), machine_type=VALUES(machine_type)"); $st->execute([$company_id,$eid,$adate,$mno,$mtype]); $u = $ctx['user']; // Required by activity helper log_activity([ 'company_id' => $company_id, 'user_id' => $user_id, 'module' => 'attendance', 'page' => basename(__FILE__), 'action' => 'UPDATE', 'entity_type' => 'assignment', 'entity_id' => $eid, 'description' => "Machine assignment updated: ID {$eid} to {$mtype} {$mno} on {$adate}" ]); echo json_encode(['ok'=>true,'saved'=>'mach']); exit; } echo json_encode(['ok'=>false,'err'=>'bad kind']); exit; }catch(Throwable $e){ echo json_encode(['ok'=>false,'err'=>$e->getMessage()]); exit; } } /* ---------- 6) Page filters / defaults ---------- */ $adate = gv('date', date('Y-m-d')); $prev = date('Y-m-d', strtotime($adate.' -1 day')); $next = date('Y-m-d', strtotime($adate.' +1 day')); $statusF = gv('status','ACTIVE'); // ACTIVE | ALL $deptF = (int)gv('dept_id',0); // 0 = all $payF = gv('payroll','ALL'); // payroll_pref filter $mode = gv('mode','attendance'); // 'attendance' | 'work' /* ---------- 7) Reference lists (departments) ---------- */ $deps = []; try{ $q = $pdo->prepare('SELECT id,name FROM company_departments WHERE company_id=? ORDER BY name'); $q->execute([$company_id]); $deps = $q->fetchAll(PDO::FETCH_ASSOC) ?: []; }catch(Throwable $e){} /* ---------- 8) Load employees from company_employee_master (scoped) ---------- */ $where = ["cem.company_id = :cid"]; $param = [':cid' => $company_id]; /* status: attempt to use is_active if present; fallback handled by NOT adding filter */ try { $colCheck = $pdo->query("SHOW COLUMNS FROM company_employee_master LIKE 'is_active'")->fetchColumn(); $has_is_active = (bool)$colCheck; } catch(Throwable $e) { $has_is_active = false; } if ($statusF !== 'ALL') { if ($has_is_active) { $where[] = 'cem.is_active = :st'; $param[':st'] = ($statusF === 'ACTIVE') ? 1 : 0; } else { // fallback: don't filter if is_active unknown } } if ($deptF > 0) { $where[] = 'cem.department_id = :d'; $param[':d'] = $deptF; } /* payroll filter join only if requested */ $payFilterJoin = ''; if ($payF !== 'ALL') { $payFilterJoin = " LEFT JOIN employee_salary_rules r ON r.company_id = cem.company_id AND r.employee_id = cem.id "; $where[] = 'r.payroll_pref = :pp'; $param[':pp'] = $payF; } /* work mode: restrict to department name containing 'loom' (best-effort) */ $modeJoin = ''; if ($mode === 'work') { $where[] = 'LOWER(cd.name) LIKE :loom'; $param[':loom'] = '%loom%'; } /* final SQL: left join departments, optionally salary rules (only used above if $payF used) */ $sql = "SELECT cem.id AS id, cem.name AS name, cem.department_id AS department_id, COALESCE(cd.name,'') AS dept_name, COALESCE(r.salary_type,'') AS salary_type, COALESCE(r.payroll_pref,'') AS payroll_pref FROM company_employee_master cem LEFT JOIN company_departments cd ON cd.company_id = cem.company_id AND cd.id = cem.department_id {$payFilterJoin} WHERE " . implode(' AND ', $where) . " ORDER BY cem.name"; $st = $pdo->prepare($sql); $st->execute($param); $rows = $st->fetchAll(PDO::FETCH_ASSOC); /* ---------- 9) Existing attendance/machine values for the selected date ---------- */ $aid = []; $mach = []; if ($rows) { $ids = array_column($rows,'id'); if (!empty($ids)) { $in = '(' . implode(',', array_map('intval',$ids)) . ')'; $a = $pdo->prepare("SELECT employee_id,attendance FROM attendance_days WHERE company_id=? AND adate=? AND employee_id IN $in"); $a->execute([$company_id,$adate]); foreach($a as $r){ $aid[(int)$r['employee_id']] = (float)$r['attendance']; } $m = $pdo->prepare("SELECT employee_id,machine_no FROM attendance_assignments WHERE company_id=? AND adate=? AND employee_id IN $in"); $m->execute([$company_id,$adate]); foreach($m as $r){ $mach[(int)$r['employee_id']] = (string)$r['machine_no']; } } } /* ---------- 10) Machine lists (defaults or from machines table) ---------- */ $loomList = range(1,12); $rapierList = range(1,12); try{ $has = $pdo->query("SHOW TABLES LIKE 'machines'")->fetchColumn(); if ($has) { $ms = $pdo->prepare("SELECT * FROM machines WHERE company_id=? ORDER BY id"); $ms->execute([$company_id]); $tmpL=[]; $tmpR=[]; foreach($ms as $mr){ $text = ''; foreach(['machine_no','number','no','code','name','id'] as $k){ if(array_key_exists($k,$mr) && $mr[$k]!==null && $mr[$k]!==''){ $text=(string)$mr[$k]; break; } } $num = preg_replace('/[^0-9]/','',$text); if($num==='') continue; $num = (int)$num; $dname = ''; foreach(['machine_type','type','department','dept','dept_name','department_name'] as $k){ if(array_key_exists($k,$mr) && $mr[$k]!==null){ $dname = strtolower((string)$mr[$k]); break; } } if(strpos($dname,'rapier')!==false){ $tmpR[$num]=true; } elseif(strpos($dname,'loom')!==false){ $tmpL[$num]=true; } else{ if(array_key_exists('department_id',$mr) && $mr['department_id']){ $dn = $pdo->prepare("SELECT name FROM company_departments WHERE company_id=? AND id=?"); $dn->execute([$company_id,(int)$mr['department_id']]); $name = strtolower((string)($dn->fetchColumn() ?: '')); if(strpos($name,'rapier')!==false) $tmpR[$num]=true; else $tmpL[$num]=true; } else { $tmpL[$num]=true; } } } if($tmpL) { $loomList = array_keys($tmpL); sort($loomList); } if($tmpR) { $rapierList = array_keys($tmpR); sort($rapierList); } } }catch(Throwable $e){ /* ignore */ } /* ---------- Header include (choose a header file if present) ---------- */ $header_paths = [__DIR__.'/partials/header.php', __DIR__.'/../partials/header.php', __DIR__.'/public/partials/header.php']; $header_file = null; foreach ($header_paths as $p) { if (is_file($p)) { $header_file = $p; break; } } /* Page-specific CSS path (you said you saved it) */ $page_css = '/erp/public/assets/css/pages/attendance_entry.css'; $main_css = '/erp/public/assets/css/main.css?v=20250917'; ?><!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Attendance — <?= h($company_name) ?></title> <!-- Try to load main.css early (header.php usually does it; this is extra safe) --> <link rel="stylesheet" href="<?= htmlspecialchars($main_css) ?>"> <link rel="stylesheet" href="<?= htmlspecialchars($page_css) ?>?v=20250917"> </head> <body> <?php if ($header_file) include_once $header_file; ?> <div class="wrap" style="padding:12px 16px; max-width:920px; margin:0 auto;"> <!-- Header: date pager --> <div class="card" style="padding:8px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center"> <a class="btn-ghost" href="?date=<?= h($prev) ?>&mode=<?= h($mode) ?>&status=<?= h($statusF) ?>&dept_id=<?= (int)$deptF ?>&payroll=<?= h($payF) ?>">◀</a> <div><b><?= date('d M Y', strtotime($adate)) ?></b> | <?= date('l', strtotime($adate)) ?></div> <a class="btn-ghost" href="?date=<?= h($next) ?>&mode=<?= h($mode) ?>&status=<?= h($statusF) ?>&dept_id=<?= (int)$deptF ?>&payroll=<?= h($payF) ?>">▶</a> </div> <!-- Tabs --> <div class="row" style="margin-bottom:8px;align-items:center;gap:8px;overflow:auto"> <a class="chip <?= $mode==='attendance'?'primary':'' ?>" href="?mode=attendance&date=<?= h($adate) ?>&status=<?= h($statusF) ?>&dept_id=0&payroll=<?= h($payF) ?>">Attendance</a> <a class="chip <?= $mode==='work'?'primary':'' ?>" href="?mode=work&date=<?= h($adate) ?>&status=<?= h($statusF) ?>&dept_id=0&payroll=<?= h($payF) ?>">Work Basis</a> </div> <?php if (empty($rows)): ?> <div class="small">No employees</div> <?php else: ?> <?php foreach ($rows as $r): $eid = (int)$r['id']; $dep = strtolower($r['dept_name'] ?? ''); $cur = array_key_exists($eid,$aid) ? (float)$aid[$eid] : 1.0; $isPresent = $cur > 0; $stype = strtolower((string)($r['salary_type'] ?? '')); $pp = strtolower((string)($r['payroll_pref'] ?? '')); $isAttendanceBased = ( strpos($stype,'attend')!==false || strpos($pp,'attend')!==false || in_array($stype, ['attendance','day','daily','perday'], true) ); $showBottom = $isPresent && $isAttendanceBased; $isLoom = strpos($dep,'loom')!==false; $isRap = strpos($dep,'rapier')!==false; $showMachines = $showBottom && ( ($mode==='attendance' && ($isLoom || $isRap)) || ($mode==='work' && $isLoom) ); $mtype = $isLoom ? 'Loom' : 'Rapier'; $selM = $mach[$eid] ?? ''; ?> <div class="card" data-eid="<?= $eid ?>" style="margin-bottom:10px;padding:0;overflow:hidden"> <div style="background:var(--primary);color:#fff;padding:8px 12px;font-weight:700"><?= h($r['dept_name'] ?? '') ?></div> <div class="row" style="background:var(--primary);color:#fff;padding:12px;display:flex;justify-content:space-between;align-items:center"> <div style="display:flex;align-items:center;gap:10px"> <div class="avatar" style="width:28px;height:28px;border-radius:999px;background:#CDEFE0;border:2px solid #A3E6B2"></div> <div style="font-weight:700;color:#fff"><?= h($r['name']) ?></div> </div> <div class="toggle <?= $isPresent?'present':'absent' ?>" data-eid="<?= $eid ?>" style="cursor:pointer"> <span class="lblL">Absent</span> <div class="knob"></div> <span class="lblR">Present</span> </div> </div> <?php if ($showBottom): ?> <div class="bar present" style="padding:10px 12px"> <?php if ($mode==='attendance' && $isAttendanceBased): ?> <div class="muted">Attendance</div> <div class="chips" data-kind="attv" data-eid="<?= $eid ?>" style="margin-top:6px"> <?php $opts=['1'=>1,'H'=>0.5,'2'=>2,'1.5'=>1.5]; foreach($opts as $k=>$v): ?> <button type="button" class="chip <?= ($cur==$v?'on':'') ?>" data-val="<?= $v ?>"><?= $k ?></button> <?php endforeach; ?> </div> <?php endif; ?> <?php if ($showMachines): ?> <div class="muted" style="margin-top:8px">Machine No.</div> <div class="chips" data-kind="mach" data-eid="<?= $eid ?>" data-mtype="<?= h($mtype) ?>" style="margin-top:6px"> <?php $list = $isLoom ? $loomList : $rapierList; foreach($list as $n): $n=(string)$n; ?> <button type="button" class="mchip <?= ($selM===$n?'on':'') ?>" data-val="<?= h($n) ?>"><?= h($n) ?></button> <?php endforeach; ?> </div> <?php endif; ?> </div> <?php endif; ?> </div> <?php endforeach; ?> <?php endif; ?> </div> <?php /* Footer include (if exists) */ $footer_paths = [__DIR__.'/partials/footer.php', __DIR__.'/../partials/footer.php', __DIR__.'/public/partials/footer.php']; foreach ($footer_paths as $fp) { if (is_file($fp)) { include_once $fp; break; } } ?> <script> /* -------------------- JS (autosave) -------------------- */ function postQuick(data){ return fetch(location.href, { method:'POST', headers:{'X-Requested-With':'fetch','Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, body:new URLSearchParams(data) }).then(r=>r.json()).catch(()=>({ok:false})); } /* Toggle present/absent */ document.querySelectorAll('.toggle').forEach(tg=>{ const eid = tg.getAttribute('data-eid'); tg.addEventListener('click', async ()=>{ const isAbsent = tg.classList.contains('absent'); const makePresent = isAbsent; const present = makePresent ? 1 : 0; await postQuick({action:'quick',kind:'att',date:'<?= h($adate) ?>',eid:eid,present:present}); tg.classList.toggle('present', present===1); tg.classList.toggle('absent', present===0); const bar = tg.closest('.card')?.querySelector('.bar'); if(bar){ bar.style.display = (present===1 ? 'block' : 'none'); bar.classList.toggle('present', present===1); bar.classList.toggle('absent', present===0); } }); }); /* Attendance value chips */ document.querySelectorAll('.chips[data-kind="attv"] .chip').forEach(btn=>{ btn.addEventListener('click', async ()=>{ const group = btn.parentElement; const eid = group.getAttribute('data-eid'); const val = btn.getAttribute('data-val'); group.querySelectorAll('.chip').forEach(x=>x.classList.remove('on')); btn.classList.add('on'); await postQuick({action:'quick',kind:'att_val',date:'<?= h($adate) ?>',eid:eid,val:val}); }); }); /* Machine chips */ document.querySelectorAll('.chips[data-kind="mach"] .mchip').forEach(btn=>{ btn.addEventListener('click', async ()=>{ const group = btn.parentElement; const eid = group.getAttribute('data-eid'); const mtype = group.getAttribute('data-mtype'); const val = btn.getAttribute('data-val'); group.querySelectorAll('.mchip').forEach(x=>x.classList.remove('on')); btn.classList.add('on'); await postQuick({action:'quick',kind:'mach',date:'<?= h($adate) ?>',eid:eid,mtype:mtype,machine_no:val}); }); }); </script> </body> </html>