« Back to History
production_entry3.php
|
20260723_000646.php
Initial Domain Snapshot
Copy Code
<?php /* ====================================================================== SECTION 0: Auth + JSON helpers + Fatal catcher ====================================================================== */ header('X-Frame-Options: SAMEORIGIN'); error_reporting(E_ALL); ini_set('display_errors','0'); require __DIR__ . '/../modules/auth/auth.php'; require_login(); $u = auth_user(); $COMPANY_ID = (int)$u['company_id']; $USER_ID = (int)$u['id']; function jexit($arr, $code=200){ if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8'); http_response_code($code); } echo json_encode($arr, JSON_UNESCAPED_UNICODE); exit; } register_shutdown_function(function(){ $e = error_get_last(); if ($e && in_array($e['type'], [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR])) { jexit(['ok'=>false,'msg'=>'Fatal','detail'=>$e['message'],'file'=>basename($e['file']??''),'line'=>$e['line']??null],500); } }); /* ====================================================================== SECTION 1: DB bootstrap ====================================================================== */ if (!isset($GLOBALS['pdo']) || !$GLOBALS['pdo']) { require __DIR__ . '/../core/db.php'; // must set $GLOBALS['pdo'] } if (!isset($GLOBALS['pdo']) || !$GLOBALS['pdo']) { jexit(['ok'=>false,'msg'=>'DB connect fail'],500); } /** @var PDO $pdo */ $pdo = $GLOBALS['pdo']; $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); /* ====================================================================== SECTION 2: AJAX router (same file) — ?act=... ====================================================================== */ $act = $_GET['act'] ?? ''; if ($act) { $company_id = $COMPANY_ID; // helper: fetch khata row by code $get_khata = function($code) use($pdo,$company_id){ $s=$pdo->prepare("SELECT id, code, machine_from, machine_to FROM khatas WHERE company_id=? AND code=? AND is_active=1 LIMIT 1"); $s->execute([$company_id, trim($code)]); return $s->fetch(PDO::FETCH_ASSOC); }; // helper: resolve quality_id from quality name (quality_rates) $resolve_quality_id = function($qname) use($pdo,$company_id){ if(!$qname) return 0; $sql="SELECT quality_id FROM quality_rates WHERE company_id=? AND TRIM(LOWER(quality_name))=TRIM(LOWER(?)) ORDER BY updated_at DESC, id DESC LIMIT 1"; $st=$pdo->prepare($sql); $st->execute([$company_id,$qname]); $qid = (int)$st->fetchColumn(); return $qid ?: 0; }; try { switch ($act) { /* -------------------------------------------------------------- SECTION 2A: Options — machines list for khata GET: ?act=options&khata=MST1 -------------------------------------------------------------- */ case 'options': { $khata_code = $_GET['khata'] ?? ''; if ($khata_code==='') jexit(['ok'=>false,'msg'=>'Missing khata']); $kh = $get_khata($khata_code); if(!$kh) jexit(['ok'=>false,'msg'=>"Khata not found: $khata_code"]); $from=(int)$kh['machine_from']; $to=(int)$kh['machine_to']; $machines=[]; for($i=$from; $i<=$to; $i++) $machines[]=$i; jexit([ 'ok'=>true, 'khata'=>['id'=>(int)$kh['id'],'from'=>$from,'to'=>$to], 'machines'=>$machines ]); } /* -------------------------------------------------------------- SECTION 2B: Machine defaults — filtered + full dropdown + fixed GET: ?act=machine_defaults&khata=MST1&machine=36 -------------------------------------------------------------- */ case 'machine_defaults': { $khata_code = $_GET['khata'] ?? ''; $machine_no = (int)($_GET['machine'] ?? 0); if ($khata_code==='' || $machine_no<=0) jexit(['ok'=>false,'msg'=>'Missing params']); $kh = $get_khata($khata_code); if(!$kh) jexit(['ok'=>false,'msg'=>"Khata not found: $khata_code"]); $khata_id = (int)$kh['id']; // (A) FILTERED list: only those whose range covers this machine $sql = "SELECT id, karigar_name, CAST(machine_from AS UNSIGNED) AS mf, CAST(machine_to AS UNSIGNED) AS mt FROM loom_karigar_master WHERE company_id=:c AND khata_id=:k AND is_active=1 AND :m BETWEEN CAST(IFNULL(machine_from,:m0) AS UNSIGNED) AND CAST(IFNULL(machine_to, :m1) AS UNSIGNED) ORDER BY (machine_from IS NULL), mf, karigar_name"; $st=$pdo->prepare($sql); $st->execute([':c'=>$company_id, ':k'=>$khata_id, ':m'=>$machine_no, ':m0'=>$machine_no, ':m1'=>$machine_no]); $filtered=[]; while($r=$st->fetch(PDO::FETCH_ASSOC)){ $label=$r['karigar_name']; if ($r['mf'] && $r['mt']) $label.=" {$r['mf']}-{$r['mt']}"; $filtered[]=['id'=>(int)$r['id'],'name'=>$label]; } // (B) KHATA-WIDE list: full dropdown (labels use full names) $st2=$pdo->prepare("SELECT id, karigar_name, CAST(machine_from AS UNSIGNED) AS mf, CAST(machine_to AS UNSIGNED) AS mt FROM loom_karigar_master WHERE company_id=? AND khata_id=? AND is_active=1 ORDER BY (machine_from IS NULL), mf, karigar_name"); $st2->execute([$company_id,$khata_id]); $khata_karigars=[]; while($r2=$st2->fetch(PDO::FETCH_ASSOC)){ $label=$r2['karigar_name']; if ($r2['mf'] && $r2['mt']) { $label .= " {$r2['mf']}-{$r2['mt']}"; } $khata_karigars[]=['id'=>(int)$r2['id'],'name'=>$label]; } // (C) Last-used fixed pair (by machine_id) $fixed=[]; $q=$pdo->prepare("SELECT lines_json FROM loom_production_entry WHERE company_id=? AND khata_id=? AND machine_id=? ORDER BY entry_date DESC, id DESC LIMIT 1"); $q->execute([$company_id,$khata_id,$machine_no]); if($row=$q->fetch(PDO::FETCH_ASSOC)){ $lines=json_decode($row['lines_json']??'[]',true); if(is_array($lines)){ usort($lines,fn($a,$b)=>($b['meter']??0)<=>($a['meter']??0)); foreach($lines as $ln){ if(isset($ln['karigar_id'])) $fixed[]=(int)$ln['karigar_id']; if(count($fixed)>=2) break; } } } if(count($fixed)<2){ foreach($filtered as $opt){ if(!in_array($opt['id'],$fixed,true)) $fixed[]=$opt['id']; if(count($fixed)>=2) break; } } jexit([ 'ok'=>true, 'khata'=>['id'=>(int)$kh['id'],'from'=>(int)$kh['machine_from'],'to'=>(int)$kh['machine_to']], 'all_karigars'=>$filtered, 'khata_karigars'=>$khata_karigars, 'fixed_karigars'=>$fixed ]); } /* -------------------------------------------------------------- SECTION 2C: Next Taka — KHATA-WISE last + 1 (for initial fill only) GET: ?act=taka_next&khata_id=1 -------------------------------------------------------------- */ case 'taka_next': { $khata_id = (int)($_GET['khata_id'] ?? 0); if ($khata_id<=0) jexit(['ok'=>false,'msg'=>'Missing khata_id']); $s=$pdo->prepare("SELECT COALESCE(MAX(taka_no),0) FROM loom_production_entry WHERE company_id=? AND khata_id=?"); $s->execute([$company_id,$khata_id]); $last=(int)$s->fetchColumn(); jexit(['ok'=>true,'next_taka_no'=>$last+1]); } /* -------------------------------------------------------------- SECTION 2D: Quality by machine (auto) + quality_id from quality_rates GET: ?act=quality_by_machine&machine_no=36 -------------------------------------------------------------- */ case 'quality_by_machine': { $machine_no = (int)($_GET['machine_no'] ?? 0); if ($machine_no<=0) jexit(['ok'=>false,'msg'=>'Missing machine_no']); $code = sprintf('loom %03d',$machine_no); $s=$pdo->prepare("SELECT quality, quality_pasaria, pasaria_date FROM loom_quality_master WHERE company_id=? AND (machine_code=? OR machine_id=?) ORDER BY updated_at DESC, id DESC LIMIT 1"); $s->execute([$company_id,$code,$machine_no]); if($row=$s->fetch(PDO::FETCH_ASSOC)){ $qname = trim($row['quality'] ?? ''); $qid = $resolve_quality_id($qname); jexit(['ok'=>true,'quality'=>$qname,'quality_id'=>$qid,'quality_pasaria'=>$row['quality_pasaria'],'pasaria_date'=>$row['pasaria_date']]); } else { jexit(['ok'=>false,'msg'=>'Quality not found']); } } /* -------------------------------------------------------------- SECTION 2E: Submit POST JSON: { entry:{ quality_id:number|0, quality_name:string, ... , lines:[{karigar_id,meter,date}]} } NOTE: single entry with multi-date lines; Taka does NOT auto +1 on Add Date -------------------------------------------------------------- */ case 'submit': { $in = json_decode(file_get_contents('php://input'), true); if(!$in || !isset($in['entry']) || !is_array($in['entry'])) jexit(['ok'=>false,'msg'=>'Bad JSON'],400); $e = $in['entry']; $date_for_header = trim($e['entry_date'] ?? ''); $machine = (int)($e['machine_id'] ?? 0); $khata = (int)($e['khata_id'] ?? 0); $qName = trim($e['quality_name'] ?? ''); $qId = (int)($e['quality_id'] ?? 0); $taka = (int)($e['taka_no'] ?? 0); $extra = (float)($e['extra_mtr'] ?? 0); $remark = trim($e['remark'] ?? ''); $lines = $e['lines'] ?? []; if(!$date_for_header || !$machine || !$khata || empty($lines)){ jexit(['ok'=>false,'msg'=>'Missing date/machine/khata/lines'],400); } // Resolve quality_id: prefer numeric, else from quality_rates by name if($qId<=0){ $qId = $resolve_quality_id($qName); if($qId<=0){ jexit(['ok'=>false,'msg'=>'Quality ID not found from quality_rates for name: '.$qName],400); } } // normalize lines (per-line date provided) $norm=[]; $sum=0; foreach($lines as $ln){ $kid = (int)($ln['karigar_id'] ?? 0); $m = (float)($ln['meter'] ?? 0); $ld = trim((string)($ln['date'] ?? $date_for_header)); if($kid>0 && $m>0 && $ld!==''){ $norm[] = ['karigar_id'=>$kid,'meter'=>round($m,2),'date'=>$ld]; $sum += $m; } } if(empty($norm)) jexit(['ok'=>false,'msg'=>'No valid karigar lines'],400); // If taka missing, take next once (no +1 on add date) if($taka<=0){ $q=$pdo->prepare("SELECT COALESCE(MAX(taka_no),0) FROM loom_production_entry WHERE company_id=? AND khata_id=?"); $q->execute([$company_id,$khata]); $taka=(int)$q->fetchColumn()+1; } $meter_total=round($sum+$extra,2); // Optional weight factor (try beam_qualities by id or code name) $weight=0; try{ $qf=$pdo->prepare("SELECT IFNULL(mtr_to_kg,0) FROM beam_qualities WHERE company_id=? AND (id=? OR quality_code=?) LIMIT 1"); $qf->execute([$company_id,$qId,$qName]); $factor=(float)$qf->fetchColumn(); if($factor>0) $weight=round($meter_total*$factor,3); }catch(Throwable $__) {} $ins=$pdo->prepare("INSERT INTO loom_production_entry (company_id,entry_date,machine_id,khata_id,quality_id,taka_no,pbn,sbn,extra_mtr,lines_json,meter_total,weight,remark,created_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); $ins->execute([ $company_id,$date_for_header,$machine,$khata,$qId,$taka,'','',$extra, json_encode($norm,JSON_UNESCAPED_UNICODE),$meter_total,$weight,$remark,$USER_ID ]); jexit(['ok'=>true,'inserted_id'=>$pdo->lastInsertId(),'taka_no'=>$taka,'quality_id'=>$qId]); } default: jexit(['ok'=>false,'msg'=>'Unknown act'],404); } } catch(Throwable $e){ jexit(['ok'=>false,'msg'=>'Server error','detail'=>$e->getMessage()],500); } } /* ====================================================================== SECTION 3: Desktop page render (no ?act=...) ====================================================================== */ ?> <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Loom Production Entry (Desktop)</title> <style> /* Mr. Manager inspired palette (green + gold + teal accents) */ :root{ --brand:#1b5e20; /* deep green */ --brand2:#00a37a; /* teal/green gradient mate */ --accent:#ffd700; /* gold accent */ --bg:#f6fff8; /* soft mint bg */ --card:#ffffff; --line:#e4f0e6; --text:#1e2722; --muted:#6b7a6c; --soft:#f3fbf4; --warn:#fff8cc; --pill:#f7fbff; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial} /* Full width container */ .shell{max-width:100%;margin:12px auto;padding:0 12px} .appbar{ position:sticky;top:0;z-index:5; background:linear-gradient(90deg,var(--brand),var(--brand2)); color:#fff;padding:12px 18px;font-weight:700;display:flex;align-items:center;justify-content:space-between; box-shadow:0 6px 18px rgba(0,0,0,.12); } .layout{display:grid;grid-template-columns:320px 1fr;gap:16px;align-items:start} /* Cards with subtle shadow */ .card{background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:0 10px 24px rgba(0,0,0,.06)} .card .hd{padding:12px 16px;border-bottom:1px solid var(--line);font-weight:800;display:flex;align-items:center;gap:10px} .card .hd::after{content:"";flex:1} .card .hd:before{content:"";width:8px;height:8px;border-radius:50%;background:var(--accent)} .card .bd{padding:16px} .label{font-size:12px;color:var(--muted);margin-bottom:6px} /* Inputs = professional box look */ .input,.select,button.btn,.pill{ border:1px solid var(--line); background:#fff; border-radius:12px; padding:10px 12px; font-size:14px; transition: box-shadow .15s ease, border-color .15s ease, background .15s ease; } .input:focus,.select:focus{ outline:none;border-color:#b6e2c3; box-shadow:0 0 0 3px rgba(0,163,122,.12); } .input,.select{width:100%} .row{display:grid;grid-template-columns:1fr;gap:10px} .row2{display:grid;grid-template-columns:1fr 1fr;gap:10px} .row4{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px} /* Buttons */ .btn{cursor:pointer;display:inline-flex;align-items:center;gap:8px} .btn-primary{ background:linear-gradient(180deg,#1b7a34,#1b5e20); color:#fff;border:none;box-shadow:0 6px 14px rgba(0,163,122,.25) } .btn-ghost{background:var(--soft);border-color:#d9efe1} .btn-ghost:hover{background:#e9f9ef} .btn-danger{background:#fff1f3;border-color:#ffd9df;color:#b00020} .toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center} .help{font-size:12px;color:var(--muted);margin-left:auto} .small{font-size:12px;color:var(--muted)} .pill{display:inline-flex;align-items:center;gap:6px;padding:6px 8px;background:var(--pill);border-color:#e6eef6} /* Matrix look => card-like cells (no “table” feel) */ .table-wrap{overflow:auto;border-top:1px dashed var(--line);padding-top:8px} table.matrix{width:100%;border-collapse:separate;border-spacing:12px 10px} /* spacing = card gaps */ table.matrix th, table.matrix td{border:none;padding:0} /* remove table borders */ /* Stickies but soft */ table.matrix thead th{position:sticky;top:0;z-index:1} table.matrix th:first-child, table.matrix td:first-child{position:sticky;left:0;z-index:1} /* Header cells (employee selectors) as mini-cards */ thead .emp-head{ background:#ffffff;border:1px solid var(--line);border-radius:12px; padding:8px;display:flex;gap:8px;align-items:center; box-shadow:0 10px 20px rgba(0,0,0,.05) } thead .emp-head select{min-width:220px} /* Meter input “box” feel */ .meter-cell{ width:100%;text-align:right; background:#fff;border:1px solid var(--line); border-radius:12px;padding:10px 12px; box-shadow:0 8px 16px rgba(0,0,0,.05); } .meter-cell:focus{outline:none;border-color:#c8f0d7;box-shadow:0 0 0 3px rgba(0,163,122,.12), 0 8px 16px rgba(0,0,0,.08)} /* Date pill (“25-Aug” look) + hidden native date */ .date-wrap{display:flex;align-items:center;gap:8px} .date-pill{ background:linear-gradient(180deg,#fff,#f7fbf9); border:1px solid var(--line);border-radius:999px;padding:8px 12px;font-weight:700; box-shadow:0 8px 16px rgba(0,0,0,.06) } .date-native{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none} /* keep native picker offscreen */ .btn-row-del{padding:8px 10px} /* Footer bar */ .footerbar{ position:sticky;bottom:0;z-index:3;margin-top:16px; background:rgba(255,255,255,.92);backdrop-filter:blur(8px); border:1px solid var(--line);border-radius:16px;padding:10px 14px;display:flex;gap:12px;align-items:center;justify-content:flex-end; box-shadow:0 -8px 24px rgba(0,0,0,.06) } .footerbar .meta{margin-right:auto;display:flex;gap:12px;align-items:center} .meta .box{border:1px solid var(--line);background:#fff;border-radius:12px;padding:8px 12px} .meta .box b{color:#0e8a5a} .toast{position:fixed;left:50%;transform:translateX(-50%);bottom:90px;background:var(--warn);border:1px solid #efe4a9;border-radius:999px;padding:8px 12px;font-size:12px;display:none} /* Loading */ @keyframes spin{to{transform:rotate(360deg)}} .btn.loading{opacity:.9;pointer-events:none} .btn.loading .spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.6);border-top-color:#fff;border-radius:50%;display:inline-block;animation:spin 1s linear infinite} .btn-ghost.loading .spinner{border-color:rgba(0,0,0,.3);border-top-color:#000} </style> </head> <body> <div class="appbar"> <div>Loom Production Entry (Desktop)</div> <div class="small">Employee: <?php echo htmlspecialchars($u['name'] ?? ''); ?></div> </div> <div class="shell"> <div class="layout"> <!-- ================= Left: Config ================= --> <div class="card"> <div class="hd">Config</div> <div class="bd"> <div class="row2"> <div> <div class="label">Khata</div> <select id="khata" class="select"> <option value="">Select</option> <option value="MST1">MST1</option> <option value="MST2A">MST2A</option> <option value="MST2B">MST2B</option> </select> </div> <div> <div class="label">Machine</div> <select id="machine" class="select"><option value="">—</option></select> </div> </div> <div class="row2" style="margin-top:10px"> <div> <div class="label">Quality</div> <input id="quality_name" class="input" placeholder="auto" /> <input id="quality_id" type="hidden" value="0" /> </div> <div> <div class="label">Taka No.</div> <input id="taka_no" class="input" placeholder="auto" /> </div> </div> <div class="row4" style="margin-top:12px"> <div> <div class="label">Pasaria</div> <input class="input" placeholder="—" disabled /> </div> <div> <div class="label">Pasaria Date</div> <input class="input" placeholder="—" disabled /> </div> <div> <div class="label">PBN</div> <input class="input" placeholder="—" disabled /> </div> <div> <div class="label">SBN</div> <input class="input" placeholder="—" disabled /> </div> </div> </div> </div> <!-- ================= Right: Entry Matrix ================= --> <div class="card"> <div class="hd">Entry Matrix</div> <div class="bd"> <div class="toolbar" style="margin-bottom:10px"> <button id="btnAddDate" class="btn btn-ghost">+ Add Date</button> <button id="btnAddEmp" class="btn btn-ghost">+ Add Employee</button> <button id="btnSeePatia" class="btn btn-ghost">See Patia</button> <button id="btnTakaEdit" class="btn btn-ghost">Taka Edit</button> <div class="help">Rows = Dates (left). Columns = Employees. Enter meters per cell.</div> </div> <div class="table-wrap"> <table id="matrix" class="matrix"> <thead> <tr id="headRow"> <th style="min-width:180px">Date</th> <!-- dynamic employee columns --> </tr> </thead> <tbody id="bodyRows"></tbody> </table> </div> <div style="margin-top:12px;display:flex;gap:12px;align-items:center;flex-wrap:wrap"> <div class="pill"> <span class="small" style="margin-right:6px">Extra Mtr</span> <input id="extra_mtr_global" type="number" step="0.01" value="0" style="width:120px;border:none;background:transparent;outline:none" /> </div> <div class="pill small">Weight: <span id="lblWeight" style="margin-left:6px">auto by quality</span></div> <div style="flex:1"></div> <div class="small">Remark</div> <input id="remark" class="input" placeholder="optional" style="max-width:420px" /> </div> </div> <div class="footerbar"> <div class="meta"> <div class="box small">Total Meter: <b id="lblTotal">0.00</b></div> </div> <button id="btnResetKeepDates" class="btn btn-ghost"><span>Reset (keep dates)</span></button> <button id="btnSubmit" class="btn btn-primary"><span>Submit</span></button> </div> </div> </div> </div> <div id="toast" class="toast">Submitted successfully</div> <script> /* ====================================================================== JS: helpers + state ====================================================================== */ const $ = (id) => document.getElementById(id); const toast=(m)=>{const t=$('toast');t.textContent=m;t.style.display='block';setTimeout(()=>t.style.display='none',2000);}; const today = (()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`})(); let khataMeta=null; let karigarsMaster=[]; // all options for this khata let fixedPair=[]; // [id1,id2] let employees=[]; // selected column employees: [{id}] let takaBase=0; function addDays(iso,days){ const [y,m,d]=iso.split('-').map(Number); const dt=new Date(y, m-1, d); dt.setDate(dt.getDate()+days); const p=n=>String(n).padStart(2,'0'); return `${dt.getFullYear()}-${p(dt.getMonth()+1)}-${p(dt.getDate())}`; } /* dd-MMM label (25-Aug) */ const MMM = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; function labelFromISO(iso){ if(!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ''; const [y,m,d]=iso.split('-').map(Number); return `${String(d).padStart(2,'0')}-${MMM[m-1]}`; } function setBtnLoading(btn,is){ if(is){ btn.classList.add('loading'); const txt=btn.textContent.trim(); btn.innerHTML=`<span class="spinner"></span><span> ${txt==='Submit'?'Saving…':'Working…'}</span>`; } else{ btn.classList.remove('loading'); const label=btn.id==='btnSubmit'?'Submit':btn.id==='btnResetKeepDates'?'Reset (keep dates)':btn.textContent.trim(); btn.innerHTML=`<span>${label}</span>`; } } async function fetchJSON(url,opt){ const res=await fetch(url,opt); const txt=await res.text(); let data; try{ data=JSON.parse(txt); }catch{ throw new Error(`raw: ${txt.slice(0,200)||'<<empty>>'}`); } if(!data.ok) throw new Error(data.msg||'Server error'); return data; } function recomputeTotals(){ let tot=0; document.querySelectorAll('.meter-cell').forEach(i=>{ const v=parseFloat(i.value||0); if(!isNaN(v)) tot+=v; }); const ex=parseFloat($('extra_mtr_global').value||0); if(!isNaN(ex)) tot+=ex; $('lblTotal').textContent=tot.toFixed(2); } $('extra_mtr_global').addEventListener('input',recomputeTotals); /* ====================================================================== NEW: Enter key flows & post-submit clearing ====================================================================== */ /* Move focus to same column, next date row; create new row if needed */ function focusNextSameColumn(currInput){ const td = currInput.closest('td'); const tr = td.parentElement; const rows = Array.from(document.querySelectorAll('#bodyRows tr')); const rowIdx = rows.indexOf(tr); const colIdx = Array.from(tr.children).indexOf(td) - 1; // skip date cell // next row or create one if (rowIdx < rows.length - 1) { const targetTd = rows[rowIdx + 1].children[colIdx + 1]; const nextInput = targetTd?.querySelector('.meter-cell'); if (nextInput) { nextInput.focus(); nextInput.select(); } } else { // add new date row (auto next day) then focus same column addDateRow(); const newRows = Array.from(document.querySelectorAll('#bodyRows tr')); const lastRow = newRows[newRows.length - 1]; const targetTd = lastRow.children[colIdx + 1]; const nextInput = targetTd?.querySelector('.meter-cell'); if (nextInput) { nextInput.focus(); nextInput.select(); } } } /* Key handler for meter cells */ function handleMeterEnter(e){ if (e.key === 'Enter') { e.preventDefault(); focusNextSameColumn(e.currentTarget); } } /* After-submit: clear only meters & extra/remark; keep dates + employees */ function clearMetersOnly(){ document.querySelectorAll('.meter-cell').forEach(i=> i.value = ''); $('extra_mtr_global').value = 0; $('remark').value = ''; recomputeTotals(); } /* ====================================================================== Matrix (grid) rendering ====================================================================== */ function empOptionHtml(){ const usedIds = new Set(employees.map(e=>String(e.id||''))); const opts = karigarsMaster.map(k=>{ const dis = usedIds.has(String(k.id)) ? 'disabled' : ''; return `<option value="${k.id}" ${dis}>${k.name}</option>`; }).join(''); return `<option value="">Select</option>${opts}`; } function renderHeader(){ const head = $('headRow'); // remove old emp th while(head.children.length>1) head.removeChild(head.lastElementChild); employees.forEach((emp,idx)=>{ const th=document.createElement('th'); th.innerHTML = ` <div class="emp-head"> <select class="select emp-select" data-col="${idx}">${empOptionHtml()}</select> <div class="emp-actions"> <button type="button" class="btn btn-ghost btn-col-add" title="Insert next" data-col="${idx}">+</button> <button type="button" class="btn btn-danger btn-col-del" title="Remove" data-col="${idx}">×</button> </div> </div> `; head.appendChild(th); // set select value after inserting options const sel = th.querySelector('.emp-select'); if(emp.id) sel.value = String(emp.id); }); // Bind header events head.querySelectorAll('.emp-select').forEach(sel=>{ sel.addEventListener('change', (e)=>{ const i = parseInt(e.target.dataset.col,10); employees[i].id = parseInt(e.target.value||0,10) || 0; renderHeader(); // refresh disables to avoid duplicates }); }); head.querySelectorAll('.btn-col-add').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const i=parseInt(e.currentTarget.dataset.col,10); employees.splice(i+1,0,{id:0}); addColumnCells(i+1); renderHeader(); }); }); head.querySelectorAll('.btn-col-del').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const i=parseInt(e.currentTarget.dataset.col,10); employees.splice(i,1); removeColumnCells(i); renderHeader(); recomputeTotals(); }); }); } function addColumnCells(colIndex){ // add cell to every existing row at given column index document.querySelectorAll('#bodyRows tr').forEach(tr=>{ const td=document.createElement('td'); td.innerHTML = `<input type="number" step="0.01" class="meter-cell" />`; const inp = td.querySelector('input'); inp.addEventListener('input',recomputeTotals); inp.addEventListener('keydown',handleMeterEnter); // ENTER flow if(colIndex >= tr.children.length-0) tr.appendChild(td); else tr.insertBefore(td, tr.children[colIndex+1]); // +1 to skip date cell }); } function removeColumnCells(colIndex){ document.querySelectorAll('#bodyRows tr').forEach(tr=>{ const delCell = tr.children[colIndex+1]; // +1 skip date cell if(delCell) tr.removeChild(delCell); }); } /* Date row with “25-Aug” label + hidden native date input */ function addDateRow(forceDate=''){ const body=$('bodyRows'); const last = body.lastElementChild; const iso = forceDate || (last ? addDays(last.querySelector('.date-native')?.value||today,1) : today); const tr=document.createElement('tr'); const dateTd=document.createElement('td'); dateTd.innerHTML = ` <div class="date-wrap"> <button type="button" class="date-pill"></button> <input type="date" class="date-native" value="${iso}" /> <button type="button" class="btn btn-danger btn-row-del">×</button> </div>`; tr.appendChild(dateTd); // cells for current employees employees.forEach(()=> { const td=document.createElement('td'); td.innerHTML=`<input type="number" step="0.01" class="meter-cell" />`; const inp = td.querySelector('input'); inp.addEventListener('input',recomputeTotals); inp.addEventListener('keydown',handleMeterEnter); // ENTER flow tr.appendChild(td); }); body.appendChild(tr); // link pill ↔ native date const native = tr.querySelector('.date-native'); const pill = tr.querySelector('.date-pill'); const sync = ()=>{ pill.textContent = labelFromISO(native.value); }; sync(); pill.addEventListener('click', ()=>{ if(native.showPicker){ native.showPicker(); } else { native.focus(); native.click(); } }); native.addEventListener('change', sync); tr.querySelector('.btn-row-del').addEventListener('click',()=>{ tr.remove(); recomputeTotals(); }); } /* Clear all (used on khata/machine changes) */ function clearMatrix(keepDates=false){ if(!keepDates){ $('bodyRows').innerHTML=''; } else { // keep rows, but clear meters only document.querySelectorAll('.meter-cell').forEach(i=>i.value=''); } employees=[]; renderHeader(); recomputeTotals(); } /* ====================================================================== Bindings: left config ====================================================================== */ $('btnAddDate').addEventListener('click', ()=>{ if(!$('khata').value || !$('machine').value){ alert('Select Khata and Machine first.'); return; } addDateRow(); }); $('btnAddEmp').addEventListener('click', ()=>{ if(!$('khata').value || !$('machine').value){ alert('Select Khata and Machine first.'); return; } employees.push({id:0}); addColumnCells(employees.length-1); renderHeader(); }); $('btnSeePatia').addEventListener('click', ()=>{ window.open('/erp/loom_patia.php','_blank'); }); $('btnTakaEdit').addEventListener('click', ()=>{ alert('Taka Edit will open from your existing page once wired.'); }); $('khata').addEventListener('change', async (e)=>{ const code=e.target.value; clearMatrix(false); $('taka_no').value=''; $('quality_name').value=''; $('quality_id').value='0'; $('quality_name').readOnly=false; if(!code){ $('machine').innerHTML='<option value="">—</option>'; return; } try{ const data=await fetchJSON(`?act=options&khata=${encodeURIComponent(code)}`); khataMeta=data.khata; $('machine').innerHTML='<option value="">Select</option>'+data.machines.map(m=>`<option value="${m}">${m}</option>`).join(''); }catch(err){ alert('options: '+err.message); } }); $('machine').addEventListener('change', async (e)=>{ const m=e.target.value, code=$('khata').value; if(!m||!code) return; try{ const def=await fetchJSON(`?act=machine_defaults&khata=${encodeURIComponent(code)}&machine=${m}`); karigarsMaster = def.khata_karigars; fixedPair = (def.fixed_karigars||[]).filter((v,i,a)=>a.indexOf(v)===i); const tn=await fetchJSON(`?act=taka_next&khata_id=${def.khata.id}`); takaBase = parseInt(tn.next_taka_no||0) || 0; $('taka_no').value = takaBase>0? String(takaBase):''; try{ const q=await fetchJSON(`?act=quality_by_machine&machine_no=${m}`); $('quality_name').value = q.quality || ''; $('quality_id').value = (q.quality_id||0); $('quality_name').readOnly = !!q.quality; }catch{ $('quality_name').readOnly=false; $('quality_id').value='0'; } // Prepare matrix: first date row + 2 fixed employees clearMatrix(false); if(fixedPair.length){ fixedPair.slice(0,2).forEach(id=>employees.push({id:id||0})); } else { employees.push({id:0},{id:0}); } renderHeader(); addDateRow(today); }catch(err){ alert('machine_defaults: '+err.message); } }); /* ====================================================================== Reset + Submit ====================================================================== */ $('btnResetKeepDates').addEventListener('click', ()=>{ setBtnLoading($('btnResetKeepDates'), true); try{ clearMetersOnly(); // keep dates & employees; just clear meters/extra/remark } finally { setBtnLoading($('btnResetKeepDates'), false); } }); $('btnSubmit').addEventListener('click', async ()=>{ if(!khataMeta || !khataMeta.id){ alert('Please select Khata.'); return; } const machineId = parseInt($('machine').value||0); if(!machineId){ alert('Please select Machine.'); return; } const qName = $('quality_name').value.trim(); if(!qName){ alert('Quality missing.'); return; } const qId = parseInt($('quality_id').value||0); const taka = parseInt($('taka_no').value||0); const body = $('bodyRows'); if(!body.children.length){ alert('Please add at least 1 Date.'); return; } if(!employees.length){ alert('Please add at least 1 Employee column.'); return; } const lines=[]; let header_date=''; Array.from(body.children).forEach((tr)=>{ const d = tr.querySelector('.date-native')?.value || ''; if(!d) return; if(!header_date) header_date=d; // for each employee column employees.forEach((emp,idx)=>{ const kid = parseInt(emp.id||0); if(kid>0){ const td = tr.children[idx+1]; // +1 skip date cell const m = parseFloat(td.querySelector('.meter-cell')?.value||0); if(m>0) lines.push({karigar_id:kid, meter:m, date:d}); } }); }); if(lines.length===0){ alert('Please enter meters in at least one cell.'); return; } const extra = parseFloat($('extra_mtr_global').value||0) || 0; const btn=$('btnSubmit'); setBtnLoading(btn,true); try{ const payload = { entry:{ entry_date: header_date, machine_id: machineId, khata_id: khataMeta.id, quality_id: qId, // numeric if known, else 0 quality_name: qName, // server will resolve if id missing taka_no: taka || 0, // no auto +1 on add date extra_mtr: extra, // global extra lines: lines, // multi-date lines remark: $('remark').value.trim() } }; const res = await fetchJSON(`?act=submit`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); toast('Submitted successfully'); // NEW: Do not reset dates or employees; just clear meters and focus machine clearMetersOnly(); setTimeout(()=>{ $('machine').focus(); $('machine').click(); }, 0); }catch(err){ alert('submit: '+err.message); }finally{ setBtnLoading(btn,false); } }); // live totals document.addEventListener('input', (e)=>{ if(e.target.classList.contains('meter-cell')) recomputeTotals(); }); </script> </body> </html>