« Back to History
attendance_entry.php
|
20260721_154032.php
Initial Bulk Import
Copy Code
<?php /* ============================================================================= File: /erp/attendance_entry.php (ATTENDANCE-BASED CHIPS ONLY) Purpose: Mobile supervisor UI with Present/Absent switch and autosave. NEW: Half-day/Double/1.5 + Machine chips appear ONLY for attendance-based salary employees. Work-basis = only P/A. Scope: Page-local only. No base/global setting changes. Auth: modules/auth/auth.php (session company_id) Depends: /erp/core/db.php (PDO, utf8mb4) ============================================================================= */ 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']; $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 ---------- */ $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, -- A=0, P=1, H=0.5, D=2, 1.5=1.5 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, -- Loom | Rapier UNIQUE KEY uniq_assign (company_id, employee_id, adate) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); /* ---------- 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]); } 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]); 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]); 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 ---------- */ $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 ---------- */ $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 (scoped) ---------- */ $where = ["e.company_id=:cid"]; $param = [':cid'=>$company_id]; if($statusF!=='ALL'){ $where[]='e.status=:st'; $param[':st']=$statusF; } if($deptF>0){ $where[]='e.department_id=:d'; $param[':d']=$deptF; } if($payF!=='ALL'){ $where[]='r.payroll_pref=:pp'; $param[':pp']=$payF; } if($mode==='work'){ // Work Basis tab shows only Loom dept $where[] = 'LOWER(d.name) LIKE :loom'; $param[':loom'] = '%loom%'; } $sql = "SELECT e.id,e.name,e.status,e.department_id, d.name AS dept_name, r.salary_type,r.payroll_pref FROM employees e LEFT JOIN company_departments d ON d.id=e.department_id LEFT JOIN employee_salary_rules r ON r.company_id=e.company_id AND r.employee_id=e.id WHERE ".implode(' AND ',$where)." ORDER BY e.name"; $st = $pdo->prepare($sql); $st->execute($param); $rows = $st->fetchAll(PDO::FETCH_ASSOC); /* ---------- 9) Existing values for the selected date ---------- */ $aid = []; // employee_id => attendance value $mach = []; // employee_id => machine_no 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 chips (prefer 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; // default Loom } } } if($tmpL) $loomList = array_keys($tmpL); if($tmpR) $rapierList = array_keys($tmpR); sort($loomList); sort($rapierList); } }catch(Throwable $e){ /* fallback keeps defaults */ } ?> <!doctype html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Attendance — <?= h($company_name) ?></title> <style> /* Mr manager palette */ :root{ --mm-primary: #34A853; --mm-bg: #F5FFF7; --mm-text: #222222; --mm-muted: #5F6368; --mm-accent: #A7D8DE; --red: #EF4444; --border: #D1D5DB; } *{box-sizing:border-box} body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:var(--mm-bg);margin:0;padding:12px 16px;color:var(--mm-text)} .wrap{max-width:820px;margin:0 auto} .head{display:flex;justify-content:space-between;align-items:center;background:var(--mm-primary);color:#fff;border-radius:14px;padding:10px 12px;margin-bottom:8px} .head a{color:#fff;text-decoration:none;font-weight:700} .tabs{display:flex;gap:8px;overflow:auto;margin:8px 0} .tab{padding:6px 10px;border-radius:999px;border:1px solid var(--mm-accent);background:#EAF8F0;white-space:nowrap;font-size:14px;text-decoration:none;color:#145C35} .tab.active{background:#145C35;color:#EAF8F0;border-color:#145C35} .emp{background:#FFFFFF;border:1px solid #BFE6CD;border-radius:14px;margin:10px 0;overflow:hidden;box-shadow:0 1px 2px rgba(0,0,0,0.03)} .deptBar{background:#109C5A;color:#fff;padding:8px 12px;font-weight:700} .row{display:flex;align-items:center;justify-content:space-between;gap:8px;background:#0F9B59;padding:12px} .name{font-weight:700;color:#fff} .avatar{width:28px;height:28px;border-radius:999px;background:#CDEFE0;border:2px solid #A3E6B2;overflow:hidden} .avatar img{width:100%;height:100%;object-fit:cover;display:block} .bar{background:#F3F4F6;padding:10px 12px} .chips{display:flex;gap:6px;flex-wrap:wrap} .chip, .mchip{border:1px solid var(--border);background:#fff;padding:6px 10px;border-radius:8px;cursor:pointer} .mchip{border-radius:999px} .muted{font-size:12px;color:var(--mm-muted)} /* Switch */ .toggle{position:relative;width:92px;height:28px;background:#e5e7eb;border-radius:999px;display:flex;align-items:center;justify-content:center;padding:0 6px;border:2px solid #e5e7eb} .toggle .knob{position:absolute;top:50%;transform:translateY(-50%);left:4px;width:40%;height:20px;border-radius:999px;background:#fff;border:2px solid var(--mm-primary);transition:left .18s ease, border-color .18s ease} .toggle .lblL,.toggle .lblR{position:absolute;top:50%;transform:translateY(-50%);font-weight:700;font-size:12px} .toggle .lblL{left:8px} .toggle .lblR{right:8px} .toggle.present{border-color:var(--mm-primary)} .toggle.absent{border-color:var(--red)} .toggle.present .lblR{color:var(--mm-primary)} .toggle.present .lblL{display:none} .toggle.absent .lblL{color:var(--red)} .toggle.absent .lblR{display:none} .toggle.absent .knob{left:calc(100% - 4px - 40%);border-color:var(--red)} /* chip color by state */ .bar.present .chip,.bar.present .mchip{border-color:var(--mm-primary);color:var(--mm-primary)} .bar.present .chip.on,.bar.present .mchip.on{background:var(--mm-primary);color:#fff;border-color:var(--mm-primary)} .bar.absent .chip,.bar.absent .mchip{border-color:var(--red);color:var(--red)} .bar.absent .chip.on,.bar.absent .mchip.on{background:var(--red);color:#fff;border-color:var(--red)} </style> <div class="wrap"> <!-- Header: date pager --> <div class="head"> <a 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 href="?date=<?= h($next) ?>&mode=<?= h($mode) ?>&status=<?= h($statusF) ?>&dept_id=<?= (int)$deptF ?>&payroll=<?= h($payF) ?>">▶</a> </div> <!-- Tabs --> <div class="tabs"> <a class="tab <?= $mode==='attendance'?'active':'' ?>" href="?mode=attendance&date=<?= h($adate) ?>&status=<?= h($statusF) ?>&dept_id=0&payroll=<?= h($payF) ?>">Attendance</a> <a class="tab <?= $mode==='work'?'active':'' ?>" 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="muted">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; // default Present $isPresent = $cur > 0; // --- Salary mode detection (robust) --- $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) ); $isWorkBasis = ( strpos($stype,'work')!==false || strpos($pp,'work')!==false || in_array($stype, ['meter','permeter','taka','per_taka','piece','job','task'], true) ); // NEW RULE: Work-basis => only Present/Absent (no bottom panel at all) $showBottom = $isPresent && $isAttendanceBased; // Machine chips visible only for attendance-based (per your rule) $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="emp" data-eid="<?= $eid ?>"> <div class="deptBar"><?= h($r['dept_name'] ?? '') ?></div> <div class="row"> <div style="display:flex;align-items:center;gap:10px"> <div class="avatar"></div> <div class="name"><?= h($r['name']) ?></div> </div> <div class="toggle <?= $isPresent?'present':'absent' ?>" data-eid="<?= $eid ?>"> <span class="lblL">Absent</span> <div class="knob"></div> <span class="lblR">Present</span> </div> </div> <?php if($showBottom): ?> <div class="bar present"> <?php if($mode==='attendance' && $isAttendanceBased): ?> <div class="muted">Attendance</div> <div class="chips" data-kind="attv" data-eid="<?= $eid ?>"> <?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) ?>"> <?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> <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 (left=present/right=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; const res = 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); // Show/hide bottom panel if it exists (attendance-based only) const bar = tg.closest('.emp').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 (only for attendance-based; present) 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 (only for attendance-based where visible) 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>