« Back to History
loom_machine_summary.php
|
20260721_154033.php
Initial Bulk Import
Copy Code
<?php /* loom_machine_summary_inline.php Inline Add/Edit — with totals + quality control - Shows date-range H1/H2 - Shows meters as 12+25 (not sum) - Inline add accepts "12" or "12+25" - Adds quality control: top dropdown populated from quality_list and suggested by quality_by_machine - create_line accepts quality_id and inserts it into loom_production_entry - Per-karigar totals + grand total shown at bottom Backup your current file before replacing. */ header('X-Frame-Options: SAMEORIGIN'); error_reporting(E_ALL); ini_set('display_errors','0'); if (isset($_GET['debug']) && $_GET['debug']=='1') ini_set('display_errors','1'); 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; } $pdo = $GLOBALS['pdo'] ?? null; if (!$pdo) { require __DIR__ . '/core/db.php'; } $pdo = $GLOBALS['pdo']; if (!$pdo) jexit(['ok'=>false,'msg'=>'DB connect fail'],500); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); /* helpers */ function month_period_to_range($month,$period){ $parts = explode('-',$month); if(count($parts)!=2) return null; $y=intval($parts[0]); $m=intval($parts[1]); if($m<1||$m>12) return null; if($period==='H1'){ $from = sprintf('%04d-%02d-01',$y,$m); $to = sprintf('%04d-%02d-15',$y,$m); } else { $from = sprintf('%04d-%02d-16',$y,$m); $dim = (int)date('t', strtotime("$y-$m-01")); $to = sprintf('%04d-%02d-%02d',$y,$m,$dim); } return [$from,$to]; } function fetch_khata_by_code($pdo,$company_id,$code){ $st=$pdo->prepare("SELECT id,code,machine_from,machine_to FROM khatas WHERE company_id=? AND code=? AND is_active=1 LIMIT 1"); $st->execute([$company_id,trim($code)]); return $st->fetch(PDO::FETCH_ASSOC); } function fetch_assigned_karigars($pdo,$company_id,$khata_id,$machine_no){ $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=? AND khata_id=? AND is_active=1 AND ? BETWEEN CAST(IFNULL(machine_from,?) AS UNSIGNED) AND CAST(IFNULL(machine_to,?) AS UNSIGNED) ORDER BY (machine_from IS NULL), mf, karigar_name"; $st=$pdo->prepare($sql); $st->execute([$company_id,$khata_id,$machine_no,$machine_no,$machine_no]); return $st->fetchAll(PDO::FETCH_ASSOC); } function fetch_all_karigars_in_khata($pdo,$company_id,$khata_id){ $st=$pdo->prepare("SELECT id,karigar_name FROM loom_karigar_master WHERE company_id=? AND khata_id=? AND is_active=1 ORDER BY karigar_name"); $st->execute([$company_id,$khata_id]); return $st->fetchAll(PDO::FETCH_ASSOC); } /* quality helpers */ function quality_by_machine($pdo,$company_id,$machine_no){ $code = sprintf('loom %03d', $machine_no); $s=$pdo->prepare("SELECT quality 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'] ?? ''); if($qname!==''){ $st=$pdo->prepare("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->execute([$company_id,$qname]); $qid = (int)$st->fetchColumn(); return ['quality_name'=>$qname,'quality_id'=>$qid]; } } return null; } function quality_list($pdo,$company_id){ $st=$pdo->prepare("SELECT quality_id,quality_name FROM quality_rates WHERE company_id=? ORDER BY updated_at DESC, quality_name LIMIT 200"); $st->execute([$company_id]); return $st->fetchAll(PDO::FETCH_ASSOC); } /* AJAX router */ $act = $_GET['act'] ?? ''; if($act){ try { switch($act){ case 'khata_list': { try{ $st=$pdo->prepare("SELECT id,code,machine_from,machine_to FROM khatas WHERE company_id=? AND is_active=1 ORDER BY code"); $st->execute([$COMPANY_ID]); $rows = $st->fetchAll(PDO::FETCH_ASSOC); jexit(['ok'=>true,'khatas'=>$rows]); } catch(Throwable $ex) { if(isset($_GET['debug']) && $_GET['debug']=='1'){ jexit(['ok'=>false,'msg'=>'khata_list failed','detail'=>$ex->getMessage()],500); } jexit(['ok'=>false,'msg'=>'khata_list failed'],500); } } case 'khata_karigars': { $kcode = trim($_GET['khata'] ?? ''); if(!$kcode) jexit(['ok'=>false,'msg'=>'Missing khata'],400); $kh = fetch_khata_by_code($pdo,$COMPANY_ID,$kcode); if(!$kh) jexit(['ok'=>false,'msg'=>'Khata not found'],404); $rows = fetch_all_karigars_in_khata($pdo,$COMPANY_ID,$kh['id']); jexit(['ok'=>true,'karigars'=>$rows]); } case 'quality_by_machine': { $machine_no = (int)($_GET['machine_no'] ?? 0); if($machine_no<=0) jexit(['ok'=>false,'msg'=>'Missing machine_no'],400); $res = quality_by_machine($pdo,$COMPANY_ID,$machine_no); if($res) jexit(['ok'=>true,'quality'=>$res]); jexit(['ok'=>false,'msg'=>'Quality not found'],404); } case 'quality_list': { $list = quality_list($pdo,$COMPANY_ID); jexit(['ok'=>true,'qualities'=>$list]); } case 'fetch_summary': { $kcode = trim($_GET['khata'] ?? ''); $machine = (int)($_GET['machine'] ?? 0); $month = trim($_GET['month'] ?? ''); $period = trim($_GET['period'] ?? 'H1'); if(!$kcode || !$machine || !$month) jexit(['ok'=>false,'msg'=>'Missing params'],400); $kh = fetch_khata_by_code($pdo,$COMPANY_ID,$kcode); if(!$kh) jexit(['ok'=>false,'msg'=>'Khata not found'],404); $rng = month_period_to_range($month,$period); if(!$rng) jexit(['ok'=>false,'msg'=>'Invalid month/period'],400); list($from,$to) = $rng; $q = $pdo->prepare("SELECT id,taka_no,entry_date,lines_json,quality_id FROM loom_production_entry WHERE company_id=? AND khata_id=? AND machine_id=? AND entry_date BETWEEN ? AND ? ORDER BY entry_date ASC,id ASC"); $q->execute([$COMPANY_ID,(int)$kh['id'],$machine,$from,$to]); $rows = $q->fetchAll(PDO::FETCH_ASSOC); // aggregate per karigar->date $agg = []; $kar_ids = []; foreach($rows as $r){ $entry_id = (int)$r['id']; $taka_no = $r['taka_no']; $lines = json_decode($r['lines_json'] ?? '[]', true); if(!is_array($lines)) continue; foreach($lines as $ln){ $ln_date = isset($ln['date']) ? substr($ln['date'],0,10) : $r['entry_date']; $kid = (int)($ln['karigar_id'] ?? 0); $m = floatval($ln['meter'] ?? 0); if($kid<=0) continue; $kar_ids[$kid]=1; if(!isset($agg[$kid])) $agg[$kid]=[]; if(!isset($agg[$kid][$ln_date])) $agg[$kid][$ln_date]=['details'=>[],'takas'=>[],'qualities'=>[]]; $agg[$kid][$ln_date]['details'][]=['entry_id'=>$entry_id,'taka_no'=>$taka_no,'meter'=>$m,'quality_id'=>($r['quality_id']??null)]; if(!in_array($taka_no,$agg[$kid][$ln_date]['takas'],true)) $agg[$kid][$ln_date]['takas'][] = $taka_no; if($r['quality_id']) $agg[$kid][$ln_date]['qualities'][$r['quality_id']] = ($agg[$kid][$ln_date]['qualities'][$r['quality_id']] ?? 0) + $m; } } // build date list $dates=[]; $dstart=new DateTime($from); $dend=new DateTime($to); for($dt=$dstart; $dt <= $dend; $dt->modify('+1 day')) $dates[]=$dt->format('Y-m-d'); // assigned karigars for machine first $assigned = fetch_assigned_karigars($pdo,$COMPANY_ID,$kh['id'],$machine); $all_kar = fetch_all_karigars_in_khata($pdo,$COMPANY_ID,$kh['id']); $out = []; $seen=[]; foreach($assigned as $k){ $kid = (int)$k['id']; $seen[$kid]=1; $dlist=[]; foreach($dates as $dt){ if(isset($agg[$kid][$dt])){ $taks = $agg[$kid][$dt]['takas']; sort($taks,SORT_NUMERIC); $dlist[]=['date'=>$dt,'has'=>true,'taka_display'=>implode('+',$taks),'details'=>$agg[$kid][$dt]['details'],'qualities'=>$agg[$kid][$dt]['qualities']]; } else { $dlist[]=['date'=>$dt,'has'=>false,'taka_display'=>'','details'=>[],'qualities'=>[]]; } } $out[]=['karigar_id'=>$kid,'karigar_name'=>$k['karigar_name'],'dates'=>$dlist]; } // include other karigars that have data but not assigned foreach($agg as $kid=>$byd){ if(isset($seen[$kid])) continue; $st=$pdo->prepare("SELECT karigar_name FROM loom_karigar_master WHERE company_id=? AND id=? LIMIT 1"); $st->execute([$COMPANY_ID,$kid]); $r=$st->fetch(PDO::FETCH_ASSOC); $kname = $r ? $r['karigar_name'] : "K#{$kid}"; $dlist=[]; foreach($dates as $dt){ if(isset($agg[$kid][$dt])){ $taks = $agg[$kid][$dt]['takas']; sort($taks,SORT_NUMERIC); $dlist[]=['date'=>$dt,'has'=>true,'taka_display'=>implode('+',$taks),'details'=>$agg[$kid][$dt]['details'],'qualities'=>$agg[$kid][$dt]['qualities']]; } else { $dlist[]=['date'=>$dt,'has'=>false,'taka_display'=>'','details'=>[],'qualities'=>[]]; } } $out[]=['karigar_id'=>$kid,'karigar_name'=>$kname,'dates'=>$dlist]; } $kar_map=[]; foreach($all_kar as $k) $kar_map[]=['id'=>$k['id'],'name'=>$k['karigar_name']]; jexit(['ok'=>true,'from'=>$from,'to'=>$to,'dates'=>$dates,'karigars'=>$out,'all_karigars'=>$kar_map]); } case 'fetch_for_edit': { $kcode = trim($_GET['khata'] ?? ''); $machine=(int)($_GET['machine']??0); $date=trim($_GET['date'] ?? ''); $kid=(int)($_GET['karigar_id']??0); if(!$kcode||!$machine||!$date||!$kid) jexit(['ok'=>false,'msg'=>'Missing params'],400); $kh = fetch_khata_by_code($pdo,$COMPANY_ID,$kcode); if(!$kh) jexit(['ok'=>false,'msg'=>'Khata not found'],404); $st = $pdo->prepare("SELECT id,taka_no,lines_json,quality_id FROM loom_production_entry WHERE company_id=? AND khata_id=? AND machine_id=? AND entry_date=? ORDER BY id ASC"); $st->execute([$COMPANY_ID,(int)$kh['id'],$machine,$date]); $rows = $st->fetchAll(PDO::FETCH_ASSOC); $out = []; foreach($rows as $r){ $eid=(int)$r['id']; $taka=$r['taka_no']; $lines=json_decode($r['lines_json'] ?? '[]', true); if(!is_array($lines)) $lines=[]; $filtered=[]; foreach($lines as $ln){ $ln_date = isset($ln['date'])?substr($ln['date'],0,10):$date; if((int)($ln['karigar_id']??0) === $kid && $ln_date === $date){ $filtered[]=['karigar_id'=> (int)$ln['karigar_id'],'meter'=>floatval($ln['meter'] ?? 0),'date'=>$ln_date]; } } if(!empty($filtered)) $out[]=['entry_id'=>$eid,'taka_no'=>$taka,'quality_id'=>($r['quality_id']??null),'lines'=>$filtered]; } jexit(['ok'=>true,'data'=>$out]); } case 'create_line': { $in = json_decode(file_get_contents('php://input'), true); if(!$in) jexit(['ok'=>false,'msg'=>'Bad JSON'],400); $kcode = trim($in['khata'] ?? ''); $machine=(int)($in['machine']??0); $date=trim($in['date']??''); $kid=(int)($in['karigar_id']??0); $meter = floatval($in['meter'] ?? 0); $quality_id = (int)($in['quality_id'] ?? 0); if(!$kcode||!$machine||!$date||!$kid||$meter<=0) jexit(['ok'=>false,'msg'=>'Missing/invalid params'],400); $kh = fetch_khata_by_code($pdo,$COMPANY_ID,$kcode); if(!$kh) jexit(['ok'=>false,'msg'=>'Khata not found'],404); try{ $pdo->beginTransaction(); // lock maxima for takas $s = $pdo->prepare("SELECT COALESCE(MAX(taka_no),0) FROM loom_production_entry WHERE company_id=? AND khata_id=? FOR UPDATE"); $s->execute([$COMPANY_ID,(int)$kh['id']]); $last=(int)$s->fetchColumn(); $next=$last+1; $lines = json_encode([['karigar_id'=>$kid,'meter'=>round($meter,2),'date'=>substr($date,0,10)]], JSON_UNESCAPED_UNICODE); // Insert with quality_id if present (we assume column exists) $ins = $pdo->prepare("INSERT INTO loom_production_entry (company_id,entry_date,machine_id,khata_id,quality_id,taka_no,lines_json,meter_total,created_by,created_at) VALUES (?,?,?,?,?,?,?,?,?,NOW())"); $ins->execute([$COMPANY_ID,$date,$machine,(int)$kh['id'],$quality_id,$next,$lines,round($meter,2),$USER_ID]); $nid = $pdo->lastInsertId(); $pdo->commit(); jexit(['ok'=>true,'entry_id'=>$nid,'taka_no'=>$next,'meter'=>round($meter,2),'quality_id'=>$quality_id]); } catch(Throwable $e){ $pdo->rollBack(); jexit(['ok'=>false,'msg'=>'create failed','detail'=>$e->getMessage()],500); } } case 'update_entries': { $in = json_decode(file_get_contents('php://input'), true); if(!$in || !isset($in['edits']) || !is_array($in['edits'])) jexit(['ok'=>false,'msg'=>'Bad JSON'],400); $edits = $in['edits']; $pdo->beginTransaction(); try{ $updated=[]; foreach($edits as $e){ $entry_id = (int)($e['entry_id'] ?? 0); if(!$entry_id) continue; $st = $pdo->prepare("SELECT lines_json FROM loom_production_entry WHERE company_id=? AND id=? FOR UPDATE"); $st->execute([$COMPANY_ID,$entry_id]); $row=$st->fetch(PDO::FETCH_ASSOC); if(!$row) continue; $orig = json_decode($row['lines_json'] ?? '[]', true); if(!is_array($orig)) $orig=[]; $to_replace=[]; foreach($e['lines'] as $ln){ $kd=(int)($ln['karigar_id']??0); $dd=substr(trim($ln['date']??''),0,10); if($kd>0 && $dd!=='') $to_replace[]=$kd.'::'.$dd; } $new_lines=[]; foreach($orig as $ol){ $ok_k = (int)($ol['karigar_id']??0); $ok_d = substr(trim($ol['date']??''),0,10); if(in_array($ok_k.'::'.$ok_d,$to_replace,true)) continue; $new_lines[] = $ol; } foreach($e['lines'] as $ln){ $k=(int)($ln['karigar_id']??0); $m=floatval($ln['meter']??0); $d=substr(trim($ln['date']??''),0,10); if($k>0 && $d!=='') $new_lines[] = ['karigar_id'=>$k,'meter'=>round($m,2),'date'=>$d]; } $sum=0.0; foreach($new_lines as $nl) $sum += floatval($nl['meter'] ?? 0); $sum = round($sum,2); // SAFER UPDATE: don't assume updated_by/updated_at exist $upd = $pdo->prepare("UPDATE loom_production_entry SET lines_json=?, meter_total=? WHERE id=? AND company_id=?"); $upd->execute([json_encode($new_lines, JSON_UNESCAPED_UNICODE), $sum, $entry_id, $COMPANY_ID]); $updated[]=['entry_id'=>$entry_id,'meter_total'=>$sum]; } $pdo->commit(); jexit(['ok'=>true,'updated'=>$updated]); } catch(Throwable $e){ $pdo->rollBack(); jexit(['ok'=>false,'msg'=>'Update failed','detail'=>$e->getMessage()],500); } } default: jexit(['ok'=>false,'msg'=>'Unknown act'],404); } } catch(Throwable $e){ if(isset($_GET['debug']) && $_GET['debug']=='1') jexit(['ok'=>false,'msg'=>'Server error','detail'=>$e->getMessage()],500); jexit(['ok'=>false,'msg'=>'Server error'],500); } } /* FRONTEND */ ?><!doctype html> <html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Machine Summary — Inline Add/Edit (Quality + Totals)</title> <style> body{font-family:Inter,system-ui,Arial;margin:18px;background:#f7fafc;color:#0f172a} .container{max-width:1400px;margin:10px auto;padding:18px} .card{background:#fff;padding:16px;border:1px solid #e6e9ee;border-radius:10px;box-shadow:0 6px 18px rgba(15,23,42,.04)} .row{display:flex;gap:12px;align-items:end;flex-wrap:wrap} .input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #d1d5db;font-size:14px} .input.small{width:120px} .input.full{min-width:220px;width:240px} .btn{background:#1d4ed8;color:#fff;border:none;padding:8px 12px;border-radius:8px;cursor:pointer} .btn.ghost{background:#fff;color:#111;border:1px solid #d1d5db} .grid{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px} .card-kg{background:#fff;border:1px solid #e6e9ee;border-radius:10px;padding:10px;min-width:260px;max-width:360px;box-shadow:0 4px 12px rgba(15,23,42,.03)} .kg-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px} .kg-name{font-weight:800} .kg-total{font-size:13px;color:#0f172a;background:#f1f5f9;padding:4px 8px;border-radius:8px} .kg-dates{margin-top:6px} .kg-row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-top:1px dashed #f1f5f9} .kg-row:first-child{border-top:none} .kg-row .date{color:#374151;min-width:110px} .kg-row .taka{background:#eef2ff;color:#1e3a8a;padding:4px 8px;border-radius:10px;font-weight:700} .inlineMeter{width:140px;padding:6px 8px;border:1px solid #cbd5e1;border-radius:6px} .smallActions{display:flex;gap:8px;align-items:center} .placeholder{color:#6b7280;padding:12px} .totalsBar{margin-top:12px;padding:10px;background:#fff;border:1px solid #e6e9ee;border-radius:8px;display:flex;justify-content:space-between;align-items:center} @media (max-width:920px){ .grid{flex-direction:column} .card-kg{max-width:100%} } </style> </head><body> <div class="container"> <div class="card"> <h3 style="margin:0 0 10px 0">Machine Summary — Inline Add/Edit</h3> <div class="row" style="gap:10px;align-items:center"> <div> <label>Khata</label><br> <select id="khata" class="input full"><option>Loading…</option></select> </div> <div> <label>Machine</label><br> <select id="machine" class="input small"><option value="">—</option></select> </div> <div> <label>Quality</label><br> <select id="quality_select" class="input small"><option value="0">(select)</option></select> </div> <div> <label>Month</label><br> <input id="month" class="input small" type="month" /> </div> <div> <label>Period</label><br> <select id="period" class="input small"><option value="H1">H1 (1-15)</option><option value="H2">H2 (16-end)</option></select> </div> <div style="display:flex;gap:8px;align-items:center"> <button id="btnLoad" class="btn">Load</button> <button id="btnClear" class="btn ghost">Clear</button> </div> <div style="margin-left:auto"> <label style="font-size:12px;color:#374151">Add karigar:</label><br> <select id="extraKarigar" class="input small"><option value="">Select</option></select> <button id="btnAddKarigar" class="btn ghost" style="margin-left:6px">Add</button> </div> </div> <div style="margin-top:8px;color:#6b7280;font-size:13px"> Tip: पूरा date-range दिखेगा। जहां value है वह locked रहेगी (उदा. 12+25)। खाली में meter डालकर Enter दबाएँ — entry बन जाएगी और focus next date पर जाएगी। Quality select करने पर नई entries उसी quality के साथ बनेगी। </div> <div id="summaryWrap" style="margin-top:14px"></div> <div id="grandTotals" class="totalsBar" style="display:none"> <div><strong>Grand total (shown range):</strong> <span id="grandTotalValue">0</span> mtr</div> <div id="qualityLegend" style="color:#374151"></div> </div> </div> </div> <script> const $ = id=>document.getElementById(id); async function fetchJSON(url,opt){ const r = await fetch(url,opt); const t = await r.text(); try{ const j = JSON.parse(t); if(!j.ok) throw new Error(j.msg||'Server'); return j; } catch(e){ throw new Error('Bad response: '+t.slice(0,400)); } } /* load khatas & qualities */ async function loadKhatas(){ const sel = $('khata'); sel.innerHTML = '<option>Loading…</option>'; const parentRow = sel.closest('.row') || sel.parentElement; const existingRetry = parentRow ? parentRow.querySelector('.khata-retry') : null; if(existingRetry) existingRetry.remove(); try{ const j = await fetchJSON('?act=khata_list'); if(!Array.isArray(j.khatas) || j.khatas.length===0){ sel.innerHTML = '<option value="">(no khatas)</option>'; return; } sel.innerHTML = '<option value="">Select Khata</option>'; for(const k of j.khatas){ const o = document.createElement('option'); o.value=k.code; o.dataset.kid=k.id; o.dataset.from=k.machine_from; o.dataset.to=k.machine_to; o.textContent = k.code + (k.machine_from && k.machine_to ? ` — ${k.machine_from}-${k.machine_to}` : ''); sel.appendChild(o); } }catch(err){ console.error('khata_list error',err); sel.innerHTML = '<option value="">(failed to load)</option>'; if(parentRow && !parentRow.querySelector('.khata-retry')){ const btn=document.createElement('button'); btn.className='btn ghost khata-retry'; btn.textContent='Retry khata'; btn.style.marginLeft='8px'; btn.addEventListener('click',()=>{ btn.remove(); loadKhatas(); }); parentRow.appendChild(btn); } } // load quality list once try{ const ql = await fetchJSON('?act=quality_list'); const qs = $('quality_select'); qs.innerHTML = '<option value="0">(select)</option>'; for(const q of ql.qualities){ const o=document.createElement('option'); o.value=q.quality_id; o.textContent=q.quality_name; qs.appendChild(o); } }catch(e){ console.error('quality_list failed',e); } } loadKhatas(); /* when khata selected, populate machines and karigar dropdown */ $('khata').addEventListener('change', async (e)=>{ const sel = e.target.selectedOptions[0]; const ms = $('machine'); ms.innerHTML = '<option value="">—</option>'; $('extraKarigar').innerHTML = '<option value="">Select</option>'; if(!sel) return; const from = parseInt(sel.dataset.from||0) || 0; const to = parseInt(sel.dataset.to||0) || 0; if(from && to && to>=from){ for(let i=from;i<=to;i++){ const o=document.createElement('option'); o.value=i; o.textContent=i; ms.appendChild(o); } } try{ const j = await fetchJSON(`?act=khata_karigars&khata=${encodeURIComponent(sel.value)}`); $('extraKarigar').innerHTML = '<option value="">Select</option>'; for(const k of j.karigars){ const o=document.createElement('option'); o.value=k.id; o.textContent=k.karigar_name; $('extraKarigar').appendChild(o); } }catch(err){ console.error(err); } }); /* when machine selected, suggest quality_by_machine if available */ $('machine').addEventListener('change', async (e)=>{ const m = parseInt(e.target.value||0); if(!m) return; try{ const q = await fetchJSON(`?act=quality_by_machine&machine_no=${encodeURIComponent(m)}`); if(q && q.quality && q.quality.quality_id){ const sid = String(q.quality.quality_id); const qs = $('quality_select'); // if option exists, select it, else add it first then select let opt = qs.querySelector(`option[value="${sid}"]`); if(!opt){ opt = document.createElement('option'); opt.value = sid; opt.textContent = q.quality.quality_name; qs.insertBefore(opt, qs.firstChild.nextSibling); } qs.value = sid; } }catch(err){ // silent console.error('quality_by_machine', err); } }); /* Client-side Add karigar card */ $('btnAddKarigar').addEventListener('click', ()=>{ const sel = $('extraKarigar'); const vid = sel.value; if(!vid) return alert('Select karigar'); const vtxt = sel.selectedOptions[0]?.textContent || ('K#'+vid); if(document.querySelector(`.card-kg[data-kg="${vid}"]`)){ const card = document.querySelector(`.card-kg[data-kg="${vid}"]`); const inp = card.querySelector('input.inlineMeter:not([disabled])'); if(inp) { inp.focus(); inp.select && inp.select(); } return; } const summary = window.__lastSummaryData; if(!summary || !summary.dates) { alert('Please Load a khata/machine/month first'); return; } const k = { karigar_id: vid, karigar_name: vtxt, dates: summary.dates.map(dt=>({date:dt,has:false, taka_display:'', details:[], qualities:[]})) }; const wrap = $('summaryWrap'); const grid = wrap.querySelector('.grid') || (() => { const g=document.createElement('div'); g.className='grid'; wrap.appendChild(g); return g; })(); const card = renderCardForKarigar(k); grid.appendChild(card); const firstInp = card.querySelector('input.inlineMeter'); if(firstInp){ firstInp.focus(); firstInp.select && firstInp.select(); } }); /* render helpers */ function renderCardForKarigar(k){ const card = document.createElement('div'); card.className='card-kg'; card.dataset.kg=k.karigar_id; const head = document.createElement('div'); head.className='kg-head'; const name = document.createElement('div'); name.className='kg-name'; name.textContent=k.karigar_name; const totalSpan = document.createElement('div'); totalSpan.className='kg-total'; totalSpan.textContent='0 mtr'; totalSpan.dataset.total='0'; const addbtn = document.createElement('button'); addbtn.className='btn ghost'; addbtn.textContent='Add'; addbtn.onclick = ()=>{ const inputs = card.querySelectorAll('input.inlineMeter'); for(const inpt of inputs){ if(!inpt.disabled && inpt.value===''){ inpt.focus(); return; } } alert('No empty date to add in this range — try changing period/month'); }; const leftHead = document.createElement('div'); leftHead.style.display='flex'; leftHead.style.gap='10px'; leftHead.style.alignItems='center'; leftHead.appendChild(name); leftHead.appendChild(totalSpan); head.appendChild(leftHead); head.appendChild(addbtn); card.appendChild(head); const datesWrap = document.createElement('div'); datesWrap.className='kg-dates'; k.dates.forEach(d=>{ const row = document.createElement('div'); row.className='kg-row'; row.dataset.date=d.date; const left = document.createElement('div'); left.className='date'; left.textContent=d.date; const right = document.createElement('div'); right.className='smallActions'; if(d.has){ const meters = (d.details && d.details.length) ? d.details.map(x => Number(x.meter || 0)) : []; const meterStr = meters.length === 0 ? '' : meters.map(m => (Math.round(m*100)/100)).join('+'); const disp = document.createElement('div'); disp.className='taka'; disp.textContent = meterStr || d.taka_display || ''; const edit = document.createElement('button'); edit.className='btn'; edit.textContent='Edit'; edit.dataset.date = d.date; edit.dataset.kid = k.karigar_id; edit.onclick = ()=>startEditInline(k.karigar_id,d.date,edit); right.appendChild(disp); right.appendChild(edit); } else { const inp = document.createElement('input'); inp.type='text'; inp.className='inlineMeter'; inp.placeholder='mtr (e.g. 12 or 12+25)'; inp.dataset.kid = k.karigar_id; inp.dataset.date = d.date; inp.addEventListener('keydown', (ev)=>{ if(ev.key==='Enter') handleQuickAddEnter(ev.target); }); right.appendChild(inp); } row.appendChild(left); row.appendChild(right); datesWrap.appendChild(row); }); card.appendChild(datesWrap); // compute card total updateCardTotal(card); return card; } function updateCardTotal(card){ const vals = Array.from(card.querySelectorAll('.kg-row .taka')).map(d=>d.textContent.trim()).filter(x=>x!==''); let sum=0; vals.forEach(s=>{ // s could be "12" or "12+25" s.split('+').forEach(p=>{ const n = parseFloat(p); if(!isNaN(n)) sum += n; }); }); sum = Math.round(sum*100)/100; const span = card.querySelector('.kg-total'); if(span){ span.textContent = sum + ' mtr'; span.dataset.total = String(sum); } updateGrandTotal(); } function updateGrandTotal(){ const cards = Array.from(document.querySelectorAll('.card-kg')); let grand = 0; cards.forEach(c=>{ const t = parseFloat(c.querySelector('.kg-total')?.dataset.total || 0); grand += (isNaN(t)?0:t); }); grand = Math.round(grand*100)/100; $('grandTotalValue').textContent = grand; $('grandTotals').style.display = grand>0 ? 'flex' : 'none'; // also show quality legend: selected quality name const qsel = $('quality_select'); const qtxt = qsel.selectedOptions[0]?.textContent || ''; $('qualityLegend').textContent = qsel.value>0 ? (`Quality: ${qtxt}`) : ''; } /* render karigars */ function renderKarigars(data){ window.__lastSummaryData = data; const wrap = $('summaryWrap'); wrap.innerHTML = ''; if(!data || !data.karigars || !data.karigars.length){ wrap.innerHTML = '<div class="placeholder">No data for selected range.</div>'; $('grandTotals').style.display='none'; return; } const grid = document.createElement('div'); grid.className='grid'; data.karigars.forEach(k=>{ const card = renderCardForKarigar(k); grid.appendChild(card); }); wrap.appendChild(grid); // recalc totals document.querySelectorAll('.card-kg').forEach(c=>updateCardTotal(c)); } /* Load summary */ $('btnLoad').addEventListener('click', async ()=>{ const kh=$('khata').value.trim(); const m=parseInt($('machine').value||0); const mo=$('month').value; const per=$('period').value; if(!kh||!m||!mo) return alert('Fill khata/machine/month'); const wrap = $('summaryWrap'); wrap.innerHTML = '<div class="placeholder">Loading…</div>'; const btn = $('btnLoad'); btn.disabled=true; btn.textContent='Loading…'; try{ const j = await fetchJSON(`?act=fetch_summary&khata=${encodeURIComponent(kh)}&machine=${m}&month=${encodeURIComponent(mo)}&period=${encodeURIComponent(per)}`); renderKarigars(j); // populate extraKarigar with all_karigars const ek = $('extraKarigar'); ek.innerHTML = '<option value="">Select</option>'; (j.all_karigars || []).forEach(k=>{ const o=document.createElement('option'); o.value=k.id; o.textContent=k.name; ek.appendChild(o); }); // update quality legend (selected) updateGrandTotal(); }catch(err){ wrap.innerHTML = `<div class="placeholder" style="color:#b91c1c">Error: ${err.message}</div>`; } finally { btn.disabled=false; btn.textContent='Load'; } }); /* Quick add inline: accepts "12" or "12+25", creates sequential entries, updates DOM and moves focus to next date */ async function handleQuickAddEnter(inp){ const raw = (inp.value||'').trim(); if(!raw) return alert('Enter meter > 0'); const parts = raw.split('+').map(x=>x.trim()).filter(x=>x!==''); const meters = parts.map(p => { const n = parseFloat(p); return isNaN(n) ? null : n; }); if(meters.some(m=>m===null || m<=0)) return alert('Enter valid meter numbers, e.g. 12 or 12+25'); const kid = parseInt(inp.dataset.kid||0,10); const date = inp.dataset.date; const kh = $('khata').value.trim(); const m = parseInt($('machine').value||0); const qid = parseInt($('quality_select').value||0); if(!kh||!m) return alert('Missing khata/machine'); if(!qid || qid<=0) if(!confirm('No quality selected — continue with quality_id=0?')) return; inp.disabled = true; try{ const created = []; for(const meter of meters){ const payload = { khata: kh, machine: m, date: date, karigar_id: kid, meter: meter, quality_id: qid }; const resp = await fetchJSON('?act=create_line', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) }); created.push(resp); } // update DOM for this row const row = inp.closest('.kg-row'); const right = row.querySelector('.smallActions'); const createdMeters = created.map(c => Number(c.meter || 0)); const meterStr = createdMeters.map(mv => (Math.round(mv*100)/100)).join('+'); right.innerHTML = ''; const disp = document.createElement('div'); disp.className='taka'; disp.textContent = meterStr; const editBtn = document.createElement('button'); editBtn.className='btn'; editBtn.textContent='Edit'; editBtn.dataset.date = date; editBtn.dataset.kid = kid; editBtn.onclick = ()=>startEditInline(kid,date,editBtn); right.appendChild(disp); right.appendChild(editBtn); // update card total & grand total const card = row.closest('.card-kg'); updateCardTotal(card); // focus next input focusNextDateInput(row); }catch(err){ alert('Add failed: '+err.message); inp.disabled = false; } } /* focus next date input for same card */ function focusNextDateInput(currentRow){ const card = currentRow.closest('.card-kg'); if(!card) return; const rows = Array.from(card.querySelectorAll('.kg-row')); const idx = rows.indexOf(currentRow); for(let i=idx+1;i<rows.length;i++){ const nextInp = rows[i].querySelector('input.inlineMeter'); if(nextInp){ nextInp.focus(); nextInp.select && nextInp.select(); return; } } } /* Inline edit */ async function startEditInline(kid,date,triggerBtn){ const kh=$('khata').value.trim(); const m=parseInt($('machine').value||0); if(!kh||!m) return alert('Missing khata/machine'); try{ const j = await fetchJSON(`?act=fetch_for_edit&khata=${encodeURIComponent(kh)}&machine=${m}&date=${encodeURIComponent(date)}&karigar_id=${encodeURIComponent(kid)}`); const row = triggerBtn.closest('.kg-row'); const right = row.querySelector('.smallActions'); right.innerHTML=''; if(!j.data || !j.data.length){ right.innerHTML='<div class="placeholder">No editable entries</div>'; return; } const inputsMeta = []; j.data.forEach(entry=>{ const group = document.createElement('div'); group.style.marginBottom='6px'; const lbl = document.createElement('div'); lbl.style.fontWeight='700'; lbl.textContent = `Taka #${entry.taka_no} (Entry ${entry.entry_id})`; group.appendChild(lbl); entry.lines.forEach(ln=>{ const r = document.createElement('div'); r.style.display='flex'; r.style.gap='6px'; r.style.alignItems='center'; r.style.marginBottom='4px'; const inpt = document.createElement('input'); inpt.type='number'; inpt.step='0.01'; inpt.className='inlineMeter'; inpt.value = ln.meter; inputsMeta.push({ entry_id: entry.entry_id, karigar_id: ln.karigar_id, date: ln.date, el: inpt }); r.appendChild(inpt); group.appendChild(r); }); right.appendChild(group); }); const saveBtn = document.createElement('button'); saveBtn.className='btn'; saveBtn.textContent='Save'; const cancelBtn = document.createElement('button'); cancelBtn.className='btn ghost'; cancelBtn.textContent='Cancel'; saveBtn.onclick = async ()=>{ const map = {}; inputsMeta.forEach(meta=>{ const val = parseFloat(meta.el.value||0) || 0; if(!map[meta.entry_id]) map[meta.entry_id]=[]; map[meta.entry_id].push({ karigar_id: meta.karigar_id, meter: val, date: meta.date }); }); const edits = Object.keys(map).map(eid=>({ entry_id: parseInt(eid,10), lines: map[eid] })); if(!edits.length) return alert('Nothing to save'); saveBtn.disabled=true; saveBtn.textContent='Saving…'; try{ const resp = await fetchJSON('?act=update_entries', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({edits}) }); // refresh the single card by re-fetching summary and replacing card try{ const kh2=$('khata').value.trim(), m2=parseInt($('machine').value||0), mo2=$('month').value, per2=$('period').value; const s = await fetchJSON(`?act=fetch_summary&khata=${encodeURIComponent(kh2)}&machine=${m2}&month=${encodeURIComponent(mo2)}&period=${encodeURIComponent(per2)}`); window.__lastSummaryData = s; const grid = document.querySelector('#summaryWrap .grid'); if(grid){ s.karigars.forEach(k=>{ const existing = grid.querySelector(`.card-kg[data-kg="${k.karigar_id}"]`); const newCard = renderCardForKarigar(k); if(existing) existing.replaceWith(newCard); else grid.appendChild(newCard); }); } else { renderKarigars(s); } }catch(e){ // fallback: full load $('btnLoad').click(); } }catch(err){ alert('Save failed: '+err.message); } finally { saveBtn.disabled=false; saveBtn.textContent='Save'; } }; cancelBtn.onclick = ()=>$('btnLoad').click(); const actions = document.createElement('div'); actions.style.marginTop='6px'; actions.appendChild(saveBtn); actions.appendChild(cancelBtn); right.appendChild(actions); }catch(err){ alert('Edit load failed: '+err.message); } } /* Clear */ $('btnClear').addEventListener('click', ()=>{ $('khata').value=''; $('machine').innerHTML='<option value="">—</option>'; $('month').value=''; $('summaryWrap').innerHTML=''; $('grandTotals').style.display='none'; $('khata').focus(); }); </script> </body></html>