« Back to History
employee_deduction_manage.php
|
20260921_175631.php
Initial Domain Snapshot
Copy Code
<?php /* ============================================================================= File: /erp/employee_deduction_manage.php Purpose: Employee Deduction (−) — Add • Edit • Delete • Filter Notes: - Uses project auth/bootstrap (no global changes). - Header/footer included from /erp/partials/header.php and /erp/partials/footer.php. - Styling from /erp/public/assets/css/main.css (no inline styles). - Uses session-based flash messages for consistent global notifications. ============================================================================ */ /* ---------- BOOTSTRAP + AUTH ---------- */ require __DIR__ . '/modules/auth/auth.php'; require_login(); require_once __DIR__ . '/modules/activity/activity_logger.php'; $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'; } error_reporting(E_ALL); ini_set('display_errors','1'); /* ---------- CSRF ---------- */ if (session_status() !== PHP_SESSION_ACTIVE) session_start(); if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16)); function csrf_field(){ echo '<input type="hidden" name="csrf" value="'.htmlspecialchars($_SESSION['csrf']).'">'; } function check_csrf(){ if (($_POST['csrf'] ?? '') !== ($_SESSION['csrf'] ?? '')) { http_response_code(400); exit('Bad CSRF'); } } /* ---------- Idempotent schema-heal (LOCAL) ---------- */ $pdo->exec("CREATE TABLE IF NOT EXISTS employee_deduction ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, company_id BIGINT UNSIGNED NOT NULL, employee_id BIGINT UNSIGNED NOT NULL, employee_name VARCHAR(191) NOT NULL, entry_date DATE NOT NULL, amount DECIMAL(12,2) NOT NULL DEFAULT 0, notes VARCHAR(255) NULL, created_by BIGINT UNSIGNED NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NULL DEFAULT NULL, INDEX(company_id), INDEX(employee_id), INDEX(entry_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); /* ---------- Helpers ---------- */ function j($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function money0($n){ return number_format((float)$n, 2, '.', ''); } function redirect_self($extra=''){ $q = $_GET; if($extra) parse_str($extra, $tmp); if(!empty($tmp)) $q = array_merge($q,$tmp); $url = strtok($_SERVER['REQUEST_URI'],'?'); if(!empty($q)) $url .= '?'.http_build_query($q); header("Location: $url"); exit; } /* ---------- Filters (department first, then employee) ---------- */ $F_dept = isset($_REQUEST['f_dept']) ? trim($_REQUEST['f_dept']) : ''; // department name (string) $F_emp = isset($_REQUEST['f_emp']) ? (int)$_REQUEST['f_emp'] : 0; $F_month = isset($_REQUEST['f_month'])? $_REQUEST['f_month'] : ''; // YYYY-MM /* ---------- Departments list (for filter dropdown) ---------- */ $deptStmt = $pdo->prepare(" SELECT DISTINCT COALESCE(NULLIF(TRIM(department_name),''),'(Unassigned)') AS dept FROM company_employee_master WHERE company_id = ? ORDER BY dept ASC "); $deptStmt->execute([$COMPANY_ID]); $DEPARTMENTS = array_map(function($r){ return $r['dept']; }, $deptStmt->fetchAll(PDO::FETCH_ASSOC)); /* ---------- Employees for dropdown (respecting department filter if set) ---------- */ $empSql = " SELECT id, COALESCE(name,'') AS name, COALESCE(employee_code,'') AS code, COALESCE(department_name,'') AS dept FROM company_employee_master WHERE company_id = ? AND (is_active = 1 OR is_active IS NULL) "; $empParams = [$COMPANY_ID]; if ($F_dept !== '') { $deptValue = ($F_dept === '(Unassigned)') ? '' : $F_dept; $empSql .= " AND COALESCE(department_name,'') = ?"; $empParams[] = $deptValue; } $empSql .= " ORDER BY name ASC"; $empStmt = $pdo->prepare($empSql); $empStmt->execute($empParams); $EMPLOYEES = $empStmt->fetchAll(PDO::FETCH_ASSOC); $EMP_BY_ID = []; foreach($EMPLOYEES as $e){ $EMP_BY_ID[(int)$e['id']] = $e; } /* ---------- Create / Update / Delete (POST handling BEFORE header include) ---------- */ $flash = ''; $err = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // preserve posted f_dept so redirect keeps filter state $posted_f_dept = $_POST['f_dept'] ?? ''; } if (($_POST['action'] ?? '') === 'create') { check_csrf(); try{ $employee_id = (int)($_POST['employee_id'] ?? 0); $entry_date = trim($_POST['entry_date'] ?? ''); $amount = (float)($_POST['amount'] ?? 0); $notes = trim($_POST['notes'] ?? ''); if ($employee_id<=0) throw new Exception('Employee required'); if (!$entry_date) throw new Exception('Entry date required'); if ($amount<=0) throw new Exception('Amount must be > 0'); // Try to get name from company_employee_master (fallback to provided list) $emp_name = ''; $st = $pdo->prepare("SELECT COALESCE(name,'') AS name FROM company_employee_master WHERE id = ? AND company_id = ?"); $st->execute([$employee_id, $COMPANY_ID]); $row = $st->fetch(PDO::FETCH_ASSOC); if ($row && $row['name'] !== '') { $emp_name = $row['name']; } else { $emp_name = isset($EMP_BY_ID[$employee_id]) ? ($EMP_BY_ID[$employee_id]['name'] ?? '') : ''; if ($emp_name==='') $emp_name = 'Employee#'.$employee_id; } $stmt = $pdo->prepare(" INSERT INTO employee_deduction (company_id, employee_id, employee_name, entry_date, amount, notes, created_by, updated_at) VALUES (?,?,?,?,?,?,?, NOW()) "); $stmt->execute([$COMPANY_ID,$employee_id,$emp_name,$entry_date,$amount,$notes,$USER_ID]); activity_log([ 'company_id' => $COMPANY_ID, 'user_id' => $USER_ID, 'module' => 'employee', 'action_name' => 'create', 'entity_type' => 'employee_deduction', 'entity_id' => (int)$pdo->lastInsertId(), 'remarks' => 'Created employee deduction' ]); $flash = 'Deduction added.'; }catch(Exception $e){ $err = $e->getMessage(); } } if (($_POST['action'] ?? '') === 'update') { check_csrf(); try{ $id = (int)($_POST['id'] ?? 0); $employee_id = (int)($_POST['employee_id'] ?? 0); $entry_date = trim($_POST['entry_date'] ?? ''); $amount = (float)($_POST['amount'] ?? 0); $notes = trim($_POST['notes'] ?? ''); if ($id<=0) throw new Exception('Invalid row'); if ($employee_id<=0) throw new Exception('Employee required'); if (!$entry_date) throw new Exception('Entry date required'); if ($amount<=0) throw new Exception('Amount must be > 0'); // fetch name robustly $emp_name = ''; $st = $pdo->prepare("SELECT COALESCE(name,'') AS name FROM company_employee_master WHERE id = ? AND company_id = ?"); $st->execute([$employee_id, $COMPANY_ID]); $row = $st->fetch(PDO::FETCH_ASSOC); if ($row && $row['name'] !== '') { $emp_name = $row['name']; } else { $emp_name = isset($EMP_BY_ID[$employee_id]) ? ($EMP_BY_ID[$employee_id]['name'] ?? '') : ''; if ($emp_name==='') $emp_name = 'Employee#'.$employee_id; } $stmt = $pdo->prepare(" UPDATE employee_deduction SET employee_id=?, employee_name=?, entry_date=?, amount=?, notes=?, updated_at = NOW() WHERE id=? AND company_id=? "); $stmt->execute([$employee_id,$emp_name,$entry_date,$amount,$notes,$id,$COMPANY_ID]); activity_log([ 'company_id' => $COMPANY_ID, 'user_id' => $USER_ID, 'module' => 'employee', 'action_name' => 'edit', 'entity_type' => 'employee_deduction', 'entity_id' => $id, 'remarks' => 'Updated employee deduction' ]); $flash = 'Deduction updated.'; }catch(Exception $e){ $err = $e->getMessage(); } } if (($_POST['action'] ?? '') === 'delete') { check_csrf(); try{ $id = (int)($_POST['id'] ?? 0); if ($id<=0) throw new Exception('Invalid row'); $stmt = $pdo->prepare("DELETE FROM employee_deduction WHERE id=? AND company_id=?"); $stmt->execute([$id,$COMPANY_ID]); activity_log([ 'company_id' => $COMPANY_ID, 'user_id' => $USER_ID, 'module' => 'employee', 'action_name' => 'delete', 'entity_type' => 'employee_deduction', 'entity_id' => $id, 'remarks' => 'Deleted employee deduction' ]); $flash = 'Deduction deleted.'; }catch(Exception $e){ $err = $e->getMessage(); } } /* ---------- Use session-based flash for consistent header toast ---------- */ if (!empty($flash)) { // store as session flash used by other pages/header $_SESSION['flash_ok'] = $flash; // preserve department filter when redirecting $qs = ''; if (!empty($posted_f_dept)) $qs .= 'f_dept='.urlencode($posted_f_dept); redirect_self($qs); } if (!empty($err)) { $_SESSION['flash_err'] = $err; $qs = ''; if (!empty($posted_f_dept)) $qs .= 'f_dept='.urlencode($posted_f_dept); redirect_self($qs); } /* ---------- Fetch rows with filters ---------- */ $where = ["ex.company_id = :cid"]; $params = [':cid'=>$COMPANY_ID]; if ($F_emp>0){ $where[] = "ex.employee_id = :emp"; $params[':emp']=$F_emp; } if ($F_month){ $where[] = "DATE_FORMAT(ex.entry_date,'%Y-%m') = :mon"; $params[':mon']=$F_month; } $whereSql = implode(' AND ', $where); $sql = " SELECT ex.*, cem.employee_code FROM employee_deduction ex LEFT JOIN company_employee_master cem ON cem.id = ex.employee_id AND cem.company_id = ex.company_id WHERE $whereSql ORDER BY ex.entry_date DESC, ex.id DESC LIMIT 500 "; $stmt = $pdo->prepare($sql); $stmt->execute($params); $ROWS = $stmt->fetchAll(PDO::FETCH_ASSOC); /* ---------- Prepare employee list for add/edit select (ensure editRow employee present) ---------- */ $editRow = null; if (isset($_GET['edit'])) { $eid = (int)$_GET['edit']; $st = $pdo->prepare("SELECT * FROM employee_deduction WHERE id=? AND company_id=?"); $st->execute([$eid,$COMPANY_ID]); $editRow = $st->fetch(PDO::FETCH_ASSOC) ?: null; } // Ensure edit employee present in EMPLOYEES list so it can be selected if ($editRow) { $editEmpId = (int)$editRow['employee_id']; if ($editEmpId > 0 && !isset($EMP_BY_ID[$editEmpId])) { $q = $pdo->prepare("SELECT id, COALESCE(name,'') AS name, COALESCE(employee_code,'') AS code, COALESCE(department_name,'') AS dept FROM company_employee_master WHERE id = ? AND company_id = ?"); $q->execute([$editEmpId, $COMPANY_ID]); $found = $q->fetch(PDO::FETCH_ASSOC); if ($found) { array_unshift($EMPLOYEES, $found); $EMP_BY_ID[(int)$found['id']] = $found; } else { $EMPLOYEES = array_merge([['id'=>$editEmpId,'name'=>$editRow['employee_name'],'code'=>'','dept'=>'']], $EMPLOYEES); $EMP_BY_ID[$editEmpId] = ['id'=>$editEmpId,'name'=>$editRow['employee_name'],'code'=>'','dept'=>'']; } } } /* ---------- Compute formDeptSelected safely (prevents undefined variable notices) ---------- */ $formDeptSelected = $F_dept; if ($formDeptSelected === '' && $editRow) { $editEmpDept = ($EMP_BY_ID[(int)$editRow['employee_id']]['dept'] ?? ''); if ($editEmpDept === '') $editEmpDept = '(Unassigned)'; $formDeptSelected = $editEmpDept; } /* ---------- Now include header (which may render global toasts) ---------- */ require_once __DIR__ . '/partials/header.php'; ?> <div class="page-wrap"> <section class="page-card"> <header class="page-card__header"> <h2>Employee Deduction (−)</h2> </header> <?php // Page-local rendering of session flash if header doesn't already show it. if (!empty($_SESSION['flash_ok'])): ?> <div class="notice notice--success"><?= j($_SESSION['flash_ok']) ?></div> <?php unset($_SESSION['flash_ok']); ?> <?php endif; ?> <?php if (!empty($_SESSION['flash_err'])): ?> <div class="notice notice--error"><?= j($_SESSION['flash_err']) ?></div> <?php unset($_SESSION['flash_err']); ?> <?php endif; ?> <div class="container-fluid py-4"> <?php if (!empty($_SESSION['flash_ok'])): ?> <div class="alert alert-success alert-dismissible fade show border-0 shadow-sm" role="alert"> <i class="bi bi-check-circle-fill me-2"></i> <?= j($_SESSION['flash_ok']) ?> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> <?php unset($_SESSION['flash_ok']); ?> <?php endif; ?> <div class="card shadow-sm mb-4"> <div class="card-header bg-white py-3"> <h5 class="mb-0 fw-bold text-primary"> <?= $editRow ? '<i class="bi bi-pencil-square"></i> Edit Deduction' : '<i class="bi bi-plus-circle"></i> Add New Deduction' ?> </h5> </div> <div class="card-body"> <form method="post" class="row g-3" id="deduction-form" autocomplete="off"> <?php csrf_field(); ?> <input type="hidden" name="action" value="<?= $editRow ? 'update' : 'create' ?>"> <?php if ($editRow): ?> <input type="hidden" name="id" value="<?= (int)$editRow['id'] ?>"> <?php endif; ?> <input type="hidden" name="f_dept" value="<?= j($F_dept) ?>"> <div class="col-md-3"> <label class="form-label fw-semibold">Department</label> <select name="form_dept" id="form-dept-select" class="form-select"> <option value=""><?= j('— Select department —') ?></option> <?php foreach ($DEPARTMENTS as $d): $sel = ($formDeptSelected !== '' && $formDeptSelected === $d) ? 'selected' : ''; ?> <option value="<?= j($d) ?>" <?= $sel ?>><?= j($d) ?></option> <?php endforeach; ?> </select> </div> <div class="col-md-3"> <label class="form-label fw-semibold">Employee</label> <select name="employee_id" id="form-employee-select" class="form-select" required> <option value="">Select...</option> <?php foreach ($EMPLOYEES as $e): $eid = (int)$e['id']; $deptVal = ($e['dept'] ?? '') === '' ? '(Unassigned)' : $e['dept']; $sel = ''; if ($editRow && $eid === (int)$editRow['employee_id']) $sel = 'selected'; if (!$editRow && $F_emp === $eid) $sel = 'selected'; $label = trim(($e['name']?:'').' '.($e['code']?('['.$e['code'].']'):'').' '.($e['dept']?('— '.$e['dept']):'')); ?> <option value="<?= $eid ?>" data-dept="<?= j($deptVal) ?>" <?= $sel ?>><?= j($label) ?></option> <?php endforeach; ?> </select> </div> <div class="col-md-3"> <label class="form-label fw-semibold">Entry Date</label> <input type="date" name="entry_date" class="form-control" value="<?= j($editRow['entry_date'] ?? date('Y-m-d')) ?>" required> </div> <div class="col-md-3"> <label class="form-label fw-semibold">Amount</label> <div class="input-group"> <span class="input-group-text bg-light">₹</span> <input type="number" step="0.01" min="0" name="amount" class="form-control" value="<?= j($editRow['amount'] ?? '') ?>" required> </div> </div> <div class="col-12"> <label class="form-label fw-semibold">Notes</label> <textarea name="notes" class="form-control" rows="1" placeholder="Reason / fine / advance adj"><?= j($editRow['notes'] ?? '') ?></textarea> </div> <div class="col-12 mt-4"> <button class="btn btn-primary px-4" type="submit"> <?= $editRow ? 'Update Deduction' : 'Add Deduction' ?> </button> <?php if ($editRow): ?> <a class="btn btn-outline-secondary ms-2" href="<?= strtok($_SERVER['REQUEST_URI'],'?') ?>">Cancel</a> <?php endif; ?> </div> </form> </div> <div class="card-footer bg-light border-top py-3"> <form method="get" class="row g-2 align-items-end"> <div class="col-md-3"> <label class="form-label small text-muted fw-bold">Filter Department</label> <select name="f_dept" class="form-select form-select-sm" onchange="this.form.submit()"> <option value="">All Departments</option> <?php foreach ($DEPARTMENTS as $d): $sel = ($F_dept !== '' && $F_dept === $d) ? 'selected' : ''; ?> <option value="<?= j($d) ?>" <?= $sel ?>><?= j($d) ?></option> <?php endforeach; ?> </select> </div> <div class="col-md-3"> <label class="form-label small text-muted fw-bold">Filter Employee</label> <select name="f_emp" class="form-select form-select-sm"> <option value="0">All Employees</option> <?php $efParams = [$COMPANY_ID]; $whereDept = ""; if ($F_dept !== '') { $whereDept = " AND COALESCE(department_name,'') = ?"; $efParams[] = ($F_dept === '(Unassigned)') ? '' : $F_dept; } $empForFilterStmt = $pdo->prepare(" SELECT id, COALESCE(name,'') AS name, COALESCE(employee_code,'') AS code FROM company_employee_master WHERE company_id = ? AND (is_active = 1 OR is_active IS NULL) $whereDept ORDER BY name ASC "); $empForFilterStmt->execute($efParams); $EMP_FILTER_LIST = $empForFilterStmt->fetchAll(PDO::FETCH_ASSOC); foreach ($EMP_FILTER_LIST as $e): $sel = ($F_emp > 0 && $F_emp === (int)$e['id']) ? 'selected' : ''; ?> <option value="<?= (int)$e['id'] ?>" <?= $sel ?>><?= j(trim($e['name'].' '.($e['code']?('['.$e['code'].']'):''))) ?></option> <?php endforeach; ?> </select> </div> <div class="col-md-2"> <label class="form-label small text-muted fw-bold">Month</label> <input type="month" name="f_month" value="<?= j($F_month) ?>" class="form-control form-control-sm"> </div> <div class="col-md-4"> <button class="btn btn-sm btn-dark px-3" type="submit">Apply Filter</button> <a class="btn btn-sm btn-link text-decoration-none text-muted" href="<?= strtok($_SERVER['REQUEST_URI'],'?') ?>">Reset</a> </div> </form> </div> </div> <div class="card shadow-sm"> <div class="card-header bg-white d-flex justify-content-between align-items-center py-3"> <h5 class="mb-0 fw-bold">Deduction Entries</h5> <span class="badge bg-soft-secondary text-dark border">Showing last 500</span> </div> <div class="table-responsive"> <table class="table table-hover align-middle mb-0"> <thead class="table-light"> <tr class="text-uppercase small fw-bold"> <th class="ps-3">#</th> <th>Date</th> <th>Employee</th> <th class="text-end">Amount</th> <th>Notes</th> <th>Created</th> <th class="text-center">Actions</th> </tr> </thead> <tbody> <?php if (!$ROWS): ?> <tr><td colspan="7" class="text-center py-5 text-muted small">No deduction records found for the selection.</td></tr> <?php else: foreach ($ROWS as $r): ?> <tr> <td class="ps-3 text-muted"><?= (int)$r['id'] ?></td> <td class="fw-medium text-nowrap"><?= j(date('d-M-Y', strtotime($r['entry_date']))) ?></td> <td> <div class="fw-bold"><?= j($r['employee_name']) ?></div> <div class="small text-muted"><?= j($r['employee_code'] ? '['.$r['employee_code'].']' : '') ?></div> </td> <td class="text-end fw-bold text-danger"><?= j(money0($r['amount'])) ?></td> <td class="text-wrap small" style="max-width: 250px;"><?= j($r['notes']) ?></td> <td> <div class="small text-muted" style="line-height: 1.2;"> ID: <?= (int)$r['created_by'] ?><br> <span style="font-size: 10px;"><?= j($r['created_at']) ?></span> </div> </td> <td class="text-center"> <div class="btn-group btn-group-sm shadow-sm"> <a class="btn btn-white border" title="Edit" href="?edit=<?= (int)$r['id'] ?>&<?= ($F_dept?('f_dept='.urlencode($F_dept).'&'):'') ?><?= ($F_emp?('f_emp='.urlencode($F_emp).'&'):'') ?>"> <i class="bi bi-pencil"></i> Edit </a> <form method="post" onsubmit="return confirm('Delete this deduction?');" class="d-inline"> <?php csrf_field(); ?> <input type="hidden" name="action" value="delete"> <input type="hidden" name="id" value="<?= (int)$r['id'] ?>"> <input type="hidden" name="f_dept" value="<?= j($F_dept) ?>"> <button class="btn btn-white border text-danger" type="submit" title="Delete"> <i class="bi bi-trash"></i> </button> </form> </div> </td> </tr> <?php endforeach; endif; ?> </tbody> </table> </div> </div> <div class="mt-4 text-center py-2"> <p class="text-muted small"> <i class="bi bi-building"></i> Company ID: <span class="fw-bold text-dark"><?= $COMPANY_ID ?></span> <span class="mx-2 text-light">|</span> <i class="bi bi-person"></i> User ID: <span class="fw-bold text-dark"><?= $USER_ID ?></span> </p> </div> </div> <!-- client-side department -> employee filtering for Add form (no submit) --> <script> document.addEventListener('DOMContentLoaded', function(){ var deptSelect = document.getElementById('form-dept-select'); var empSelect = document.getElementById('form-employee-select'); if (!deptSelect || !empSelect) return; var original = Array.from(empSelect.options).map(function(o){ return { value: o.value, text: o.text, dept: o.getAttribute('data-dept') || '' }; }); function rebuild(selDept){ empSelect.innerHTML = ''; var ph = document.createElement('option'); ph.value = ''; ph.text = 'Select…'; empSelect.appendChild(ph); original.forEach(function(o){ var optDept = (o.dept === '') ? '(Unassigned)' : o.dept; var show = !selDept || (selDept === '(Unassigned)' ? optDept === '(Unassigned)' : optDept === selDept); if (show && o.value) { var opt = document.createElement('option'); opt.value = o.value; opt.text = o.text; opt.setAttribute('data-dept', o.dept); empSelect.appendChild(opt); } }); // preserve edit selection if present var current = '<?= isset($editRow) ? (int)$editRow['employee_id'] : ($F_emp ? $F_emp : '') ?>'; if (current) { for (var i=0;i<empSelect.options.length;i++){ if (String(empSelect.options[i].value) === String(current)) { empSelect.selectedIndex = i; break; } } } } // initial rebuild based on current add-form dept select rebuild(deptSelect.value || ''); deptSelect.addEventListener('change', function(){ // update hidden f_dept inputs (so POST includes it) var hiddenFDept = document.getElementsByName('f_dept'); for (var i=0;i<hiddenFDept.length;i++){ hiddenFDept[i].value = deptSelect.value; } rebuild(deptSelect.value || ''); }); }); </script> <?php require_once __DIR__ . '/partials/footer.php'; ?>