« Back to History
new_loom_salary.php
|
20260722_120325.php
Initial Domain Snapshot
Copy Code
<?php /* ============================================================================ File: /erp/new_loom_salary.php Title: New Loom Salary (aka New Loom Salary → production_entry source) Notes: - Page-local only. Do not change core/config files. - Reads per-karigar meters from production_entry.lines_json and aggregates per karigar × quality in the selected period. - Keeps original report logic, PDF export, save/send flows unchanged. ============================================================================ */ header('X-Frame-Options: SAMEORIGIN'); error_reporting(E_ALL); ini_set('display_errors',1); /* [0] Auth + DB */ 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'; } /* [0a] Composer autoload (for Dompdf) — page local */ $PDF_AVAILABLE = false; $autoload = __DIR__ . '/vendor/autoload.php'; if (is_file($autoload)) { require_once $autoload; $PDF_AVAILABLE = class_exists(\Dompdf\Dompdf::class); } /* [1] Helpers */ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function gv($k,$d=null){ return isset($_GET[$k])?trim((string)$_GET[$k]):$d; } function pv($k,$d=null){ return isset($_POST[$k])?trim((string)$_POST[$k]):$d; } function safe_json($s){ $a=json_decode($s,true); return is_array($a)?$a:[]; } function yrmo_half_range(int $y,int $m,string $half): array { // half: '1' => 1-15, '2' => 16-end, 'F' => full month $start = sprintf('%04d-%02d-01',$y,$m); $end = date('Y-m-t', strtotime($start)); if($half==='2'){ $start = sprintf('%04d-%02d-16',$y,$m); } if($half==='1'){ $end = sprintf('%04d-%02d-15',$y,$m); } return [$start,$end]; } function label_for_period(int $y,int $m,string $half,string $from,string $to): string { if ($half==='F') return 'FULL '.date('M', mktime(0,0,0,$m,1,$y)).' '.$y; if ($half==='1') return '1-15 '.date('M', mktime(0,0,0,$m,1,$y)).' '.$y; if ($half==='2') return '16-END '.date('M', mktime(0,0,0,$m,1,$y)).' '.$y; return $from.' to '.$to; } function _is_maria(PDO $pdo): bool { try { $v = (string)$pdo->query("SELECT @@version")->fetchColumn(); } catch(Throwable $e){ return false; } return stripos($v,'mariadb') !== false; } function _norm_date($s){ // '2025-08-04' or '04-08-2025' → '2025-08-04' $s = trim((string)$s); if($s==='') return null; if (preg_match('/^\d{4}-\d{2}-\d{2}$/',$s)) return $s; if (preg_match('/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/',$s,$m)) return sprintf('%04d-%02d-%02d',$m[3],$m[2],$m[1]); $ts = strtotime($s); return $ts ? date('Y-m-d',$ts) : null; } /* feature-flag (page-local): queue adjust read */ $FF_USE_QUEUE_ADJ = true; /* [2] Filters (GET) */ $year = (int)gv('year', date('Y')); $month = (int)gv('month', date('n')); $half = gv('half','F'); // default full month [$date_from,$date_to] = yrmo_half_range($year,$month,$half); $khata_id = (int)gv('khata_id', 0); $machine_id= (int)gv('machine_id', 0); $period_label = label_for_period($year,$month,$half,$date_from,$date_to); /* [3] Company name (for header) */ $company_name = "Company #$company_id"; try{ $st = $pdo->prepare("SELECT name FROM companies WHERE id=?"); $st->execute([$company_id]); $nm = $st->fetchColumn(); if($nm){ $company_name = $nm; } }catch(Exception $e){} /* [4] Masters */ $QUALS=[]; $QUAL_RATE=[]; $q = $pdo->prepare("SELECT quality_id AS id, quality_name AS name, rate FROM quality_rates WHERE company_id=? ORDER BY quality_name"); $q->execute([$company_id]); foreach($q as $r){ $id=(int)$r['id']; $QUALS[$id]=$r['name']; $QUAL_RATE[$id]=(float)$r['rate']; } $KAR_NAME=[]; $KAR_ENTRY=[]; $KAR_KHATA_CODE=[]; $KAR_KHATA_ID=[]; $st=$pdo->prepare("SELECT id, karigar_name, entry_name, khata_code, khata_id FROM loom_karigar_master WHERE company_id=? ORDER BY karigar_name"); $st->execute([$company_id]); foreach($st as $r){ $id=(int)$r['id']; $KAR_NAME[$id] = $r['karigar_name'] ?: ("Karigar #$id"); $KAR_ENTRY[$id] = $r['entry_name'] ?? ''; $KAR_KHATA_CODE[$id] = $r['khata_code'] ?? ''; $KAR_KHATA_ID[$id] = (int)($r['khata_id'] ?? 0); } $HAS_BANK=[]; try{ $st=$pdo->prepare("SELECT karigar_id FROM loom_bank_master WHERE company_id=? AND (is_default=1 OR is_default IS NULL) AND (COALESCE(account_no,'')<>'' OR COALESCE(upi_id,'')<>'')"); $st->execute([$company_id]); foreach($st as $r){ $HAS_BANK[(int)$r['karigar_id']] = true; } }catch(Exception $e){} /* ====================================================================== [5] FETCH LINES — READ FROM production_entry (page-local replacement) We expect production_entry rows to have: company_id, khata_id, machine_id, quality_id, entry_date, meter_total, lines_json We'll parse lines_json; if missing, fallback to meter_total is ignored because karigar_id is unknown. Aggregate total_meters per karigar × quality. ====================================================================== */ $_skipped_bad_date = 0; $_skipped_out_range = 0; $MTR = []; $MTR_KAR_TOT = []; $karigar_set = []; $quality_set = []; $from_ts = strtotime($date_from . ' 00:00:00'); $to_ts = strtotime($date_to . ' 23:59:59'); try { $sql = "SELECT id, company_id, khata_id, machine_id, quality_id, entry_date, meter_total, lines_json FROM production_entry WHERE company_id = :cid AND entry_date BETWEEN :d1 AND :d2"; $params = [':cid' => $company_id, ':d1' => $date_from, ':d2' => $date_to]; if ($khata_id > 0) { $sql .= " AND khata_id = :khata"; $params[':khata'] = $khata_id; } if ($machine_id > 0) { $sql .= " AND machine_id = :mc"; $params[':mc'] = $machine_id; } $sql .= " ORDER BY entry_date, machine_id, id"; $st = $pdo->prepare($sql); $st->execute($params); foreach ($st as $row) { $qid = (int)($row['quality_id'] ?? 0); $entry_date = _norm_date($row['entry_date'] ?? ''); if (!$entry_date) { $_skipped_bad_date++; continue; } $ets = strtotime($entry_date . ' 00:00:00'); if ($ets < $from_ts || $ets > $to_ts) { $_skipped_out_range++; continue; } // Parse lines_json to extract per-karigar meters $lines = safe_json($row['lines_json'] ?? ''); if (!is_array($lines) || count($lines) === 0) { // No per-karigar lines — can't attribute meters to karigar_id => skip // (optional: attempt to use meter_total if you have a mapping, but we skip to keep report exact) continue; } foreach ($lines as $li) { // Expected shape: {"karigar_id": 27, "meter": 12.5, "date":"2025-09-30"} $kid = (int)($li['karigar_id'] ?? 0); $m = (float)($li['meter'] ?? 0.0); if ($kid <= 0 || $qid <= 0 || $m <= 0.0) continue; $karigar_set[$kid] = true; $quality_set[$qid] = true; $MTR[$kid][$qid] = ($MTR[$kid][$qid] ?? 0.0) + $m; $MTR_KAR_TOT[$kid] = ($MTR_KAR_TOT[$kid] ?? 0.0) + $m; } } } catch (Throwable $e) { error_log("production_entry read failed: " . $e->getMessage()); // keep graceful fallback: empty result sets } /* Build dimensions for table */ $quality_ids = array_keys($quality_set); usort($quality_ids, fn($a,$b)=>strcasecmp($QUALS[$a]??'', $QUALS[$b]??'')); // sort by name if present $kar_list = array_keys($karigar_set); usort($kar_list, fn($a,$b)=>strcasecmp($KAR_NAME[$a]??("K$a"), $KAR_NAME[$b]??("K$b"))); /* [7] Money (Extra/Deduction/Advance) — ALWAYS merge *_data tables */ $EXTRA=[]; $DEDUCT=[]; $ADV=[]; $__adj_source = 'none'; $__adj_counts = ['extra'=>0,'deduction'=>0]; if ($FF_USE_QUEUE_ADJ) { try{ $__adj_source = 'queue'; $sq = $pdo->prepare(" SELECT karigar_id, type, SUM(amount) amt FROM salary_adjust_queue WHERE company_id=? AND person_type='karigar' AND apply_start=? AND apply_end=? GROUP BY karigar_id, type "); $sq->execute([$company_id,$date_from,$date_to]); foreach($sq as $r){ $kid = (int)$r['karigar_id']; if ($kid<=0) continue; $t = (string)$r['type']; $amt = (float)$r['amt']; if ($t==='extra'){ $EXTRA[$kid]=($EXTRA[$kid]??0)+$amt; $__adj_counts['extra']++; } if ($t==='deduction'){ $DEDUCT[$kid]=($DEDUCT[$kid]??0)+$amt; $__adj_counts['deduction']++; } } }catch(Exception $e){ $__adj_source = 'error-queue'; } } /* Util to sum from tables */ function sum_money_tbl($pdo,$company_id,$tbl,$kid,$from,$to,$date_cols=['date','entry_date','created_at'],$amt_cols=['amount','amt','value']){ foreach($date_cols as $dc){ foreach($amt_cols as $ac){ try{ $st=$pdo->prepare("SELECT SUM($ac) s FROM $tbl WHERE company_id=? AND karigar_id=? AND $dc BETWEEN ? AND ?"); $st->execute([$company_id,$kid,$from,$to]); $v=$st->fetchColumn(); if($v!==false && $v!==null) return (float)$v; }catch(Exception $e){} }} return 0.0; } /* Merge: use karigar tables (karigar_extra / karigar_deduction) + advance_data unchanged */ foreach($kar_list as $kid){ // karigar_extra uses column `entry_date` and `amount` $EXTRA[$kid] = ($EXTRA[$kid] ?? 0) + sum_money_tbl($pdo, $company_id, 'karigar_extra', $kid, $date_from, $date_to); // karigar_deduction uses column `entry_date` and `amount` $DEDUCT[$kid] = ($DEDUCT[$kid] ?? 0) + sum_money_tbl($pdo, $company_id, 'karigar_deduction', $kid, $date_from, $date_to); // advance_data left as-is (if you have karigar_advance later, change similarly) $ADV[$kid] = sum_money_tbl($pdo, $company_id, 'advance_data', $kid, $date_from, $date_to); } /* [8] Gross/Net per karigar — EXACT (no rounding) */ $GROSS=[]; $NET=[]; $bank_total=0.0; $cash_total=0.0; $grand_net=0.0; foreach($kar_list as $kid){ $g=0.0; foreach($quality_ids as $qid){ $g += ($MTR[$kid][$qid] ?? 0) * ($QUAL_RATE[$qid] ?? 0); } $GROSS[$kid]=$g; $n = $g + ($EXTRA[$kid]??0) - ($DEDUCT[$kid]??0) - ($ADV[$kid]??0); $NET[$kid] = $n; $grand_net += $n; if(!empty($HAS_BANK[$kid])) $bank_total += $n; else $cash_total += $n; } /* [9] Ensure report & deduction tables exist (for save / send) */ $pdo->exec("CREATE TABLE IF NOT EXISTS salary_reports ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, date_from DATE NOT NULL, date_to DATE NOT NULL, khata_id BIGINT NOT NULL DEFAULT 0, machine_id BIGINT NOT NULL DEFAULT 0, month INT NOT NULL, half CHAR(1) NOT NULL, year INT NOT NULL, created_by BIGINT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, meta TEXT NULL, UNIQUE KEY uniq_period (company_id, date_from, date_to, khata_id, machine_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS salary_report_items ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, report_id BIGINT NOT NULL, karigar_id BIGINT NOT NULL, karigar_name VARCHAR(120) NULL, entry_name VARCHAR(120) NULL, khata_code VARCHAR(50) NULL, meters_json TEXT NULL, total_mtr DECIMAL(12,2) DEFAULT 0, gross DECIMAL(12,2) DEFAULT 0, extra DECIMAL(12,2) DEFAULT 0, deduction DECIMAL(12,2) DEFAULT 0, advance DECIMAL(12,2) DEFAULT 0, net DECIMAL(12,2) DEFAULT 0, INDEX(report_id), INDEX(company_id, karigar_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS deduction_data ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, karigar_id BIGINT NOT NULL, khata_id BIGINT NULL, amount DECIMAL(12,2) NOT NULL DEFAULT 0, date DATE NOT NULL, remark VARCHAR(255) NULL, created_by BIGINT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX(company_id, karigar_id, date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); /* [10] SAVE / UPDATE report + apply queue */ $flash=''; $saved_report_id=null; if ($_SERVER['REQUEST_METHOD']==='POST' && pv('action')==='save_report') { $sel=$pdo->prepare("SELECT id FROM salary_reports WHERE company_id=? AND date_from=? AND date_to=? AND khata_id=? AND machine_id=? LIMIT 1"); $sel->execute([$company_id,$date_from,$date_to,$khata_id,$machine_id]); $existing_id = $sel->fetchColumn(); $meta = [ 'company_name'=>$company_name, 'quality_rate'=>$QUAL_RATE, 'quality_names'=>$QUALS, 'filters'=>['khata_id'=>$khata_id,'machine_id'=>$machine_id,'month'=>$month,'half'=>$half,'year'=>$year], 'skipped'=>['bad_date'=>$_skipped_bad_date,'out_of_range'=>$_skipped_out_range], 'rounding'=>'disabled', 'adj_source'=> $__adj_source, ]; if($existing_id){ $saved_report_id = (int)$existing_id; $up = $pdo->prepare("UPDATE salary_reports SET month=?, half=?, year=?, created_by=?, meta=? WHERE id=? AND company_id=?"); $up->execute([$month,(string)$half,$year,$user_id,json_encode($meta,JSON_UNESCAPED_UNICODE),$saved_report_id,$company_id]); $pdo->prepare("DELETE FROM salary_report_items WHERE company_id=? AND report_id=?") ->execute([$company_id,$saved_report_id]); $mode='updated'; } else { $ins=$pdo->prepare("INSERT INTO salary_reports (company_id,date_from,date_to,khata_id,machine_id,month,half,year,created_by,meta) VALUES (?,?,?,?,?,?,?,?,?,?)"); $ins->execute([$company_id,$date_from,$date_to,$khata_id,$machine_id,$month,(string)$half,$year,$user_id, json_encode($meta,JSON_UNESCAPED_UNICODE)]); $saved_report_id = (int)$pdo->lastInsertId(); $mode='saved'; } $sti = $pdo->prepare("INSERT INTO salary_report_items (company_id,report_id,karigar_id,karigar_name,entry_name,khata_code,meters_json,total_mtr,gross,extra,deduction,advance,net) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"); foreach($kar_list as $kid){ $meters = []; foreach($quality_ids as $qid){ if(($MTR[$kid][$qid] ?? 0)>0){ $meters[$qid] = round($MTR[$kid][$qid],2); } } $sti->execute([ $company_id,$saved_report_id,$kid, $KAR_NAME[$kid]??null, $KAR_ENTRY[$kid]??null, $KAR_KHATA_CODE[$kid]??null, json_encode($meters,JSON_UNESCAPED_UNICODE), round($MTR_KAR_TOT[$kid]??0,2), round($GROSS[$kid]??0,2), round($EXTRA[$kid]??0,2), round($DEDUCT[$kid]??0,2), round($ADV[$kid]??0,2), round($NET[$kid]??0,2) ]); } if ($FF_USE_QUEUE_ADJ) { try{ $ap = $pdo->prepare("UPDATE salary_adjust_queue SET is_applied=1, applied_at=NOW() WHERE company_id=? AND person_type='karigar' AND apply_start=? AND apply_end=? AND is_applied=0"); $ap->execute([$company_id,$date_from,$date_to]); $cnt = $ap->rowCount(); $flash = "Report {$mode}. ID: ".$saved_report_id." — Adjustments applied: ".$cnt; }catch(Exception $e){ $flash = "Report {$mode}. ID: ".$saved_report_id." — (apply failed, but report saved)"; } } else { $flash = "Report {$mode}. ID: ".$saved_report_id; } } /* [10B] SEND TO DEDUCTION / MARK PAID (any period, as-is) */ if ($_SERVER['REQUEST_METHOD']==='POST' && in_array(pv('action'), ['send_to_deduction','mark_paid'], true)) { $isPaid = (pv('action') === 'mark_paid'); // remark में PAID tag जोड़ने के लिए $ins = $pdo->prepare("INSERT INTO deduction_data (company_id, karigar_id, khata_id, amount, date, remark, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"); $chk = $pdo->prepare("SELECT 1 FROM deduction_data WHERE company_id=? AND karigar_id=? AND date BETWEEN ? AND ? AND remark LIKE ? LIMIT 1"); $added = 0; $tag = $isPaid ? 'PAID' : 'AUTO'; $label = $period_label; foreach ($kar_list as $kid) { $amt = (float)($NET[$kid] ?? 0); if ($amt == 0.0) continue; $like = "%Salary ($label)%"; $chk->execute([$company_id,$kid,$date_from,$date_to,$like]); if ($chk->fetchColumn()) continue; $remark = "Salary ($label) from loom_salary_report [$tag]"; $khataForKid = (int)($KAR_KHATA_ID[$kid] ?? 0); $ins->execute([$company_id,$kid,$khataForKid,round($amt,2),$date_to,$remark,$user_id]); $added++; } $flash = ($isPaid ? "Marked as PAID: " : "Sent to Deduction: ").$added." row(s) for $label."; } /* [11] PDF export */ if (gv('export') === 'pdf') { if (!$PDF_AVAILABLE) { http_response_code(500); exit('PDF export not available: install dompdf/dompdf'); } $orientation = (count($quality_ids) > 5) ? 'landscape' : 'portrait'; $css = ' body{font-family: DejaVu Sans, sans-serif; font-size:12px; color:#111;} h1{font-size:18px;margin:0 0 6px 0} .meta{font-size:12px;margin:0 0 10px 0} table{width:100%; border-collapse:collapse} th,td{border:1px solid #dfe7df; padding:6px 8px} thead th{background:#cfe9d7} tfoot td{background:#fff9e8; font-weight:700} .right{text-align:right} .small{font-size:10px;color:#666} '; $html = '<html><head><meta charset="utf-8"><style>'.$css.'</style></head><body>'; $html .= '<h1>'.h($company_name).'</h1>'; $html .= '<div class="meta"><b>Salary Report (No Rounding)</b><br>'. 'Period: '.h($date_from).' to '.h($date_to).' | Label: '.h($period_label).'</div>'; $html .= '<div class="meta">Bank: '.number_format($bank_total,2).' | Cash: '.number_format($cash_total,2). ' | <b>Total: '.number_format($grand_net,2).'</b></div>'; $html .= '<table><thead><tr><th>Karigar</th>'; foreach($quality_ids as $qid){ $html .= '<th>'.h($QUALS[$qid] ?? ("Q$qid")).'<br><span class="small">@ '.number_format($QUAL_RATE[$qid]??0,2).'</span></th>'; } $html .= '<th>Total Mtr</th><th>Gross</th><th>Extra</th><th>Deduction</th><th>Advance</th><th>Payable</th></tr></thead><tbody>'; foreach($kar_list as $kid){ $html .= '<tr><td>'.h($KAR_NAME[$kid] ?? ("Karigar #$kid")).'</td>'; foreach($quality_ids as $qid){ $m=$MTR[$kid][$qid] ?? 0; $html.='<td class="right">'.($m>0?number_format($m,2):'').'</td>'; } $html .= '<td class="right">'.number_format($MTR_KAR_TOT[$kid]??0,2).'</td>'. '<td class="right">'.number_format($GROSS[$kid]??0,2).'</td>'. '<td class="right">'.number_format($EXTRA[$kid]??0,2).'</td>'. '<td class="right">'.number_format($DEDUCT[$kid]??0,2).'</td>'. '<td class="right">'.number_format($ADV[$kid]??0,2).'</td>'. '<td class="right">'.number_format($NET[$kid]??0,2).'</td></tr>'; } $html .= '</tbody><tfoot><tr><td>TOTAL</td>'; $GRAND_GROSS=0.0; $TOT_METERS_BY_Q=[]; foreach($quality_ids as $qid){ $s=0.0; foreach($kar_list as $kid){ $s += $MTR[$kid][$qid] ?? 0; } $TOT_METERS_BY_Q[$qid]=$s; $GRAND_GROSS += $s * ($QUAL_RATE[$qid]??0.0); $html .= '<td class="right">'.number_format($s,2).'</td>'; } $grand_mtr = array_sum($TOT_METERS_BY_Q); $TOT_EXTRA = array_sum(array_intersect_key($EXTRA, array_flip($kar_list))); $TOT_DED = array_sum(array_intersect_key($DEDUCT,array_flip($kar_list))); $TOT_ADV = array_sum(array_intersect_key($ADV, array_flip($kar_list))); $html .= '<td class="right">'.number_format($grand_mtr,2).'</td>'. '<td class="right">'.number_format($GRAND_GROSS,2).'</td>'. '<td class="right">'.number_format($TOT_EXTRA,2).'</td>'. '<td class="right">'.number_format($TOT_DED,2).'</td>'. '<td class="right">'.number_format($TOT_ADV,2).'</td>'. '<td class="right">'.number_format($grand_net,2).'</td></tr></tfoot></table>'; $html .= '<div class="small" style="margin-top:8px">Skipped: Bad Date = '.(int)$_skipped_bad_date. ', Out of Range = '.(int)$_skipped_out_range.'. Adjustments: '.h($__adj_source).'; Tables merged: karigar_extra + karigar_deduction + advance_data.</div>'; $html .= '</body></html>'; $dompdf = new \Dompdf\Dompdf([ 'defaultFont' => 'DejaVu Sans', 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false, ]); $dompdf->loadHtml($html, 'UTF-8'); $dompdf->setPaper('A4', $orientation); $dompdf->render(); $fname = 'salary_report_'.$year.'-'.str_pad((string)$month,2,'0',STR_PAD_LEFT).'_'.$half.'.pdf'; header('Content-Type: application/pdf'); header('Content-Disposition: attachment; filename="'.$fname.'"'); echo $dompdf->output(); exit; } /* ============================= HTML ============================== */ ?> <!doctype html> <html> <head> <meta charset="utf-8"> <title>Salary Report (No Rounding)</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> :root{ --green:#34A853; --mint:#F5FFF7; --stone:#5F6368; --sage:#D0E8D0; --sky:#A7D8DE; --yellow:#FFF9C4; } *{box-sizing:border-box} body{margin:0;background:var(--mint);color:#222;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Arial} .wrap{max-width:1200px;margin:20px auto;padding:0 12px} .h1{color:#2c7d3f;margin:6px 0 12px} .hbar{display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;background:#e9f7ee;border:1px solid #d6f0dd;padding:12px;border-radius:12px} .hbar label{font-size:12px;color:#333;display:block;margin-bottom:4px} .hbar select{padding:8px 10px;border:1px solid #cfe8d0;border-radius:10px;background:#fff;min-width:140px} .btn{background:var(--green);color:#fff;border:none;border-radius:12px;padding:9px 14px;font-weight:600;cursor:pointer;text-decoration:none;display:inline-block} .btn.gray{background:#5F6368} .btn.warn{background:#ef4444} .badge{background:#d8f7e1;padding:6px 10px;border-radius:999px;font-size:12px;color:#2b6b3c} .card{background:#fff;border:1px solid #ecf2ec;border-radius:14px;overflow:hidden;margin-top:16px} .table{width:100%;border-collapse:separate;border-spacing:0} .table th,.table td{padding:10px 12px;border-bottom:1px solid #eef5ef} .table thead th{position:sticky;top:0;background:#cfe9d7;color:#1b4d2a;text-align:right} .table thead th:first-child{text-align:left} .table tbody td{text-align:right} .table tbody td:first-child{text-align:left} .table tfoot td{background:#fff9e8;font-weight:700} .head2{background:#b7dcd0;color:#0f4230} .subrate{font-size:11px;opacity:.8} .totalcol{background:#f3faf4;font-weight:700} .small{font-size:12px;color:#555} .right{margin-left:auto} .flash{background:#e7ffe9;border:1px solid #c7f0cb;color:#1e5a2c;padding:8px 12px;border-radius:10px;margin:10px 0;display:inline-block} .kentry{display:block;font-size:11px;color:#666} .summary{background:#f1f9f2;border:1px dashed #cde8cf;padding:8px 12px;border-radius:10px;margin:10px 0;color:#165c30} @media print { .hbar, .flash, form[action="loom_payment_export.php"] + * { display:none !important; } .wrap{max-width:100%} .card{border:none} } </style> </head> <body> <div class="wrap"> <h2 class="h1">Salary Report <span class="small">(No Rounding)</span></h2> <div class="small" style="margin-bottom:8px;"><b>Company:</b> <?= h($company_name) ?></div> <?php if($flash): ?><div class="flash"><?= h($flash) ?></div><?php endif; ?> <!-- Filters + Actions --> <form class="hbar" method="get" action=""> <div> <label>Khata</label> <select name="khata_id"> <option value="0">All</option> <?php $KHATAS=[]; try{ $kq=$pdo->prepare("SELECT DISTINCT khata_id, khata_code FROM loom_karigar_master WHERE company_id=? AND khata_id IS NOT NULL ORDER BY khata_code"); $kq->execute([$company_id]); foreach($kq as $r){ $KHATAS[(int)$r['khata_id']] = $r['khata_code']; } }catch(Exception $e){} foreach($KHATAS as $id=>$code): ?> <option value="<?= $id ?>" <?= $khata_id===$id?'selected':''?>><?= h($code) ?></option> <?php endforeach; ?> </select> </div> <div> <label>Machine</label> <select name="machine_id"> <option value="0">All Machines</option> <?php $MACH=[]; try{ $st2=$pdo->prepare("SELECT id, machine_code FROM machines WHERE company_id=? ORDER BY id"); $st2->execute([$company_id]); foreach($st2 as $r){ $MACH[(int)$r['id']] = $r['machine_code'] ?: ('MC '.$r['id']); } }catch(Exception $e){} foreach($MACH as $id=>$label): ?> <option value="<?= $id ?>" <?= $machine_id===$id?'selected':''?>><?= h($label) ?></option> <?php endforeach; ?> </select> </div> <div> <label>Month</label> <select name="month"> <?php for($m=1;$m<=12;$m++): ?> <option value="<?= $m ?>" <?= $m===$month?'selected':''?>><?= date('M', mktime(0,0,0,$m,1,$year)) ?></option> <?php endfor; ?> </select> </div> <div> <label>Period</label> <select name="half" title="Choose 1-15, 16-End, or Full month"> <option value="1" <?= $half==='1'?'selected':''?>>1–15</option> <option value="2" <?= $half==='2'?'selected':''?>>16–End</option> <option value="F" <?= $half==='F'?'selected':''?>>Full month</option> </select> </div> <div> <label>Year</label> <select name="year"> <?php for($y=date('Y')-2;$y<=date('Y')+1;$y++): ?> <option value="<?= $y ?>" <?= $y===$year?'selected':''?>><?= $y ?></option> <?php endfor; ?> </select> </div> <button class="btn right" type="submit">Show Report</button> <?php // Hidden export payload (exact net) $export_rows = []; foreach ($kar_list as $kid) { $amt = (float)($NET[$kid] ?? 0); if ($amt != 0.0) $export_rows[] = ['employee_id' => $kid, 'amount' => round($amt,2)]; } $export_payload = json_encode($export_rows, JSON_UNESCAPED_UNICODE); $pdf_qs = http_build_query([ 'khata_id'=>$khata_id,'machine_id'=>$machine_id, 'month'=>$month,'half'=>$half,'year'=>$year,'export'=>'pdf' ]); ?> <button class="btn gray" type="button" onclick="window.print()">Print</button> <a class="btn gray" href="?<?= h($pdf_qs) ?>" <?= $PDF_AVAILABLE? '' : 'onclick="alert(\'Install dompdf/dompdf first.\'); return false;"' ?>>Export PDF</a> <!-- Safe JS-based Export button (prevents nested form and encoding issues) --> <button class="btn gray" id="exportExcelBtn" type="button">Export Excel</button> <script> (function(){ const payload = <?= json_encode($export_rows, JSON_UNESCAPED_UNICODE) ?> || []; const btn = document.getElementById('exportExcelBtn'); btn.addEventListener('click', function(){ if(!payload || payload.length === 0){ alert('No payment rows to export (empty).'); return; } const form = document.createElement('form'); form.method = 'POST'; // Use relative action to avoid subpath issues; adjust if needed to '/erp/loom_payment_export.php' form.action = 'loom_payment_export.php'; form.style.display = 'none'; const inRows = document.createElement('input'); inRows.type = 'hidden'; inRows.name = 'salary_rows'; inRows.value = JSON.stringify(payload); form.appendChild(inRows); const inLabel = document.createElement('input'); inLabel.type = 'hidden'; inLabel.name = 'period_label'; inLabel.value = '<?= h($period_label) ?>'; form.appendChild(inLabel); const inStart = document.createElement('input'); inStart.type = 'hidden'; inStart.name = 'period_start'; inStart.value = '<?= h($date_from) ?>'; form.appendChild(inStart); const inEnd = document.createElement('input'); inEnd.type = 'hidden'; inEnd.name = 'period_end'; inEnd.value = '<?= h($date_to) ?>'; form.appendChild(inEnd); document.body.appendChild(form); form.submit(); }); })(); </script> </form> <!-- Summary (EXACT — no rounding) --> <div class="summary"> <b>Summary:</b> Bank / <?= number_format($bank_total,2) ?>, Cash / <?= number_format($cash_total,2) ?>, <b>Total</b> <?= number_format($grand_net,2) ?> <span class="small"> (Exact net; no rounding — Adjustments: <?= h($__adj_source) ?>; merged with karigar_extra + karigar_deduction + advance_data)</span> <?php if(strpos($__adj_source,'queue')===0): ?> <br><span class="small">Applied this period (seen in calc): Extra rows <?= (int)$__adj_counts['extra'] ?>, Deduction rows <?= (int)$__adj_counts['deduction'] ?></span> <?php endif; ?> </div> <!-- Save/Update --> <form method="post" action="" style="margin-bottom:10px; display:flex; gap:10px; align-items:center; flex-wrap:wrap"> <input type="hidden" name="action" value="save_report"> <button class="btn" type="submit">Save Salary (unique per period)</button> <span class="small">Save करने पर इस period के pending adjustments <b>Apply</b> भी हो जाएंगे।</span> </form> <!-- Deduction buttons --> <form method="post" action="" style="margin:-4px 0 8px 0; display:flex; gap:10px; align-items:center; flex-wrap:wrap"> <input type="hidden" name="action" value="send_to_deduction"> <button class="btn gray" type="submit">Send to Deduction (This Period)</button> <span class="small">इस period की payable रकम को <code>deduction_data</code> में as-is जोड़ता है.</span> </form> <form method="post" action="" style="margin:-4px 0 16px 0; display:flex; gap:10px; align-items:center; flex-wrap:wrap"> <input type="hidden" name="action" value="mark_paid"> <button class="btn warn" type="submit">Mark as Paid (This Period)</button> <span class="small">ऊपर जैसा ही, पर remark में <b>[PAID]</b> tag जुड़ता है (audit clarity).</span> </form> <div class="card"> <table class="table"> <thead> <tr> <th>Karigar</th> <?php foreach($quality_ids as $qid): ?> <th class="head2"> <?= h($QUALS[$qid] ?? ("Q$qid")) ?><br> <span class="subrate">@ <?= number_format($QUAL_RATE[$qid]??0,2) ?></span> </th> <?php endforeach; ?> <th>Total Mtr</th> <th>Gross</th> <th>Extra</th> <th>Deduction</th> <th>Advance</th> <th>Payable</th> </tr> </thead> <tbody> <?php foreach($kar_list as $kid): ?> <tr> <td> <?= h($KAR_NAME[$kid] ?? ("Karigar #$kid")) ?> <?php if(!empty($KAR_ENTRY[$kid])): ?> <span class="kentry"><?= h($KAR_ENTRY[$kid]) ?></span> <?php endif; ?> </td> <?php foreach($quality_ids as $qid): $m=$MTR[$kid][$qid] ?? 0; ?> <td><?= $m>0? number_format($m,2): '' ?></td> <?php endforeach; ?> <td class="totalcol"><?= number_format($MTR_KAR_TOT[$kid]??0,2) ?></td> <td><?= number_format($GROSS[$kid]??0,2) ?></td> <td><?= number_format($EXTRA[$kid]??0,2) ?></td> <td><?= number_format($DEDUCT[$kid]??0,2) ?></td> <td><?= number_format($ADV[$kid]??0,2) ?></td> <td class="totalcol"><?= number_format($NET[$kid]??0,2) ?></td> </tr> <?php endforeach; ?> </tbody> <tfoot> <tr> <td>TOTAL</td> <?php $TOT_METERS_BY_Q=[]; $GRAND_GROSS=0.0; foreach($quality_ids as $qid){ $s=0.0; foreach($kar_list as $kid){ $s += $MTR[$kid][$qid] ?? 0; } $TOT_METERS_BY_Q[$qid]=$s; echo "<td>".number_format($s,2)."</td>"; $GRAND_GROSS += $s * ($QUAL_RATE[$qid]??0.0); } $grand_mtr = array_sum($TOT_METERS_BY_Q); $TOT_EXTRA = array_sum(array_intersect_key($EXTRA, array_flip($kar_list))); $TOT_DED = array_sum(array_intersect_key($DEDUCT,array_flip($kar_list))); $TOT_ADV = array_sum(array_intersect_key($ADV, array_flip($kar_list))); $GRAND_NET = $grand_net; // exact ?> <td class="totalcol"><?= number_format($grand_mtr,2) ?></td> <td><?= number_format($GRAND_GROSS,2) ?></td> <td><?= number_format($TOT_EXTRA,2) ?></td> <td><?= number_format($TOT_DED,2) ?></td> <td><?= number_format($TOT_ADV,2) ?></td> <td class="totalcol"><?= number_format($GRAND_NET,2) ?></td> </tr> <tr> <td colspan="<?= 6 + count($quality_ids) ?>" class="small" style="text-align:left;padding:12px"> Amount (= Σ Meters × Rate). Rates from <code>quality_rates</code>. Lines from <code>production_entry</code>. <br>Skipped (diagnostic): Bad Date = <?= (int)$_skipped_bad_date ?>, Out of Range = <?= (int)$_skipped_out_range ?>. </td> </tr> </tfoot> </table> </div> </div> </body> </html>