« Back to History
machine_wise_production1.php
|
20260722_120325.php
Initial Domain Snapshot
Copy Code
<?php /* machine_wise_production.php Updated: Edit inputs accept "12" or "12+25" (text), Enter saves, multiple lines created on edit. - Keep backup 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); } 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 500"); $st->execute([$company_id]); return $st->fetchAll(PDO::FETCH_ASSOC); } /* ----- AJAX router ----- */ $act = $_GET['act'] ?? ''; if($act){ try { switch($act){ case 'khata_list': { $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]); } 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(); $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); $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 'add_extra': { $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'] ?? ''); if(!$kcode || !$machine || !$date) jexit(['ok'=>false,'msg'=>'Missing params'],400); $meters = []; if(isset($in['meters']) && is_array($in['meters'])){ foreach($in['meters'] as $m){ $m2 = floatval($m); if($m2>0) $meters[] = round($m2,2); } } elseif(isset($in['meter'])){ $m2 = floatval($in['meter']); if($m2>0) $meters[] = round($m2,2); } if(empty($meters)) jexit(['ok'=>false,'msg'=>'No valid meter provided'],400); $note = trim($in['note'] ?? ''); $quality_id = isset($in['quality_id']) ? (int)$in['quality_id'] : 0; $kh = fetch_khata_by_code($pdo,$COMPANY_ID,$kcode); if(!$kh) jexit(['ok'=>false,'msg'=>'Khata not found'],404); try{ $pdo->beginTransaction(); $sel = $pdo->prepare("SELECT id,lines_json,meter_total,taka_no FROM loom_production_entry WHERE company_id=? AND khata_id=? AND machine_id=? AND entry_date=? ORDER BY id DESC LIMIT 1 FOR UPDATE"); $sel->execute([$COMPANY_ID,(int)$kh['id'],$machine,$date]); if($row = $sel->fetch(PDO::FETCH_ASSOC)){ $entry_id = (int)$row['id']; $orig_lines = json_decode($row['lines_json'] ?? '[]', true); if(!is_array($orig_lines)) $orig_lines=[]; foreach($meters as $mval){ $orig_lines[] = ['karigar_id'=>0,'meter'=>round($mval,2),'date'=>substr($date,0,10),'extra_note'=>$note]; } $new_total = round((float)$row['meter_total'] + array_sum($meters),2); if($quality_id>0){ $upd = $pdo->prepare("UPDATE loom_production_entry SET lines_json=?, meter_total=?, quality_id=? WHERE id=? AND company_id=?"); $upd->execute([json_encode($orig_lines, JSON_UNESCAPED_UNICODE), $new_total, $quality_id, $entry_id, $COMPANY_ID]); } else { $upd = $pdo->prepare("UPDATE loom_production_entry SET lines_json=?, meter_total=? WHERE id=? AND company_id=?"); $upd->execute([json_encode($orig_lines, JSON_UNESCAPED_UNICODE), $new_total, $entry_id, $COMPANY_ID]); } $pdo->commit(); jexit(['ok'=>true,'action'=>'appended_extra','entry_id'=>$entry_id,'taka_no'=>$row['taka_no'],'meter_added'=>array_sum($meters),'meter_total'=>$new_total]); } $s2 = $pdo->prepare("SELECT COALESCE(MAX(taka_no),0) FROM loom_production_entry WHERE company_id=? AND khata_id=? FOR UPDATE"); $s2->execute([$COMPANY_ID,(int)$kh['id']]); $last = (int)$s2->fetchColumn(); $next = $last + 1; $lines = []; $sum=0.0; foreach($meters as $mval){ $lines[]=['karigar_id'=>0,'meter'=>round($mval,2),'date'=>substr($date,0,10),'extra_note'=>$note]; $sum += $mval; } $sum = round($sum,2); $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,json_encode($lines, JSON_UNESCAPED_UNICODE),$sum,$USER_ID]); $nid = $pdo->lastInsertId(); $pdo->commit(); jexit(['ok'=>true,'action'=>'created_extra','entry_id'=>$nid,'taka_no'=>$next,'meter_total'=>$sum]); } catch(Throwable $e){ $pdo->rollBack(); jexit(['ok'=>false,'msg'=>'add_extra failed','detail'=>$e->getMessage()],500); } } case 'update_entries': { // Now accept string meters with + (client will split); server expects edits -> each edit contains entry_id and lines: [{karigar_id, meter, date}...] $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=[]; // Build to_replace: remove lines for karigar_id::date present in incoming lines $to_replace=[]; $incoming_lines = $e['lines'] ?? []; // If client sends meters as strings with pluses, they should already be exploded client-side. But for safety, allow meter string $normalized_incoming = []; foreach($incoming_lines as $ln){ $kd = (int)($ln['karigar_id'] ?? 0); $dd = substr(trim($ln['date'] ?? ''),0,10); $mraw = $ln['meter'] ?? ''; if($kd<=0 || $dd==='') continue; // if meter is a string with +, split here if(is_string($mraw) && strpos($mraw,'+')!==false){ $parts = array_filter(array_map('trim', explode('+',$mraw))); foreach($parts as $p){ $fv = floatval($p); if($fv>0) $normalized_incoming[] = ['karigar_id'=>$kd,'meter'=>round($fv,2),'date'=>$dd]; } } else { $mv = floatval($mraw); if($mv>0) $normalized_incoming[] = ['karigar_id'=>$kd,'meter'=>round($mv,2),'date'=>$dd]; } $to_replace[] = $kd.'::'.$dd; } // Remove any original lines that match karigar_id::date present in incoming edits $new_lines = []; foreach($orig as $ol){ $ok_k = (int)($ol['karigar_id'] ?? -1); $ok_d = substr(trim($ol['date'] ?? ''),0,10); if(in_array($ok_k.'::'.$ok_d, $to_replace, true)) continue; $new_lines[] = $ol; } // Append normalized incoming lines foreach($normalized_incoming as $nl){ $new_lines[] = ['karigar_id'=> (int)$nl['karigar_id'], 'meter'=> round(floatval($nl['meter']),2), 'date'=> substr($nl['date'],0,10)]; } // re-calc sum $sum = 0.0; foreach($new_lines as $nl) $sum += floatval($nl['meter'] ?? 0); $sum = round($sum,2); $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]); // If client passed quality explicitly for this entry, update quality_id too if(isset($e['quality_id'])){ $qv = (int)$e['quality_id']; $qup = $pdo->prepare("UPDATE loom_production_entry SET quality_id=? WHERE id=? AND company_id=?"); $qup->execute([$qv,$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 (HTML + JS) ----- */ ?><!doctype html> <html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Machine — Inline Edit Fix</title> <style> body{font-family:Inter,system-ui,Arial;margin:18px;background:#f7fafc;color:#0f172a} .container{max-width:1200px;margin:10px auto;padding:18px} .card{background:#fff;padding:16px;border:1px solid #e6e9ee;border-radius:10px} .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} .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:180px;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%} } /* shortcut tip (top-right) simple style */ #shortcutTipBox{ position:fixed; top:18px; right:18px; z-index:9999; background:#ffffff; border:1px solid rgba(0,0,0,0.06); padding:8px 10px; border-radius:8px; box-shadow:0 6px 18px rgba(15,23,42,0.06); font-size:13px; color:#111827; max-width:260px; } #shortcutTipBox strong{display:block;margin-bottom:6px;font-size:13px} #shortcutTipBox kbd{background:#f3f4f6;border-radius:4px;padding:3px 6px;border:1px solid #e5e7eb;font-weight:700;margin-right:4px} </style> </head><body> <div class="container"> <div class="card"> <h3 style="margin:0 0 10px 0">Machine Inline — Edit Fix</h3> <div class="row"> <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> <!-- Period select removed as requested --> <div style="display:flex;gap:6px;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">Add karigar:</label><br><select id="extraKarigar" class="input small"><option value="">Select</option></select><button id="btnAddKarigar" class="btn ghost">Add</button></div> </div> <div style="margin-top:8px;color:#6b7280">Tip: Edit textbox accepts <code>12</code> or <code>12+25</code>. Press Enter to save (or click Save). If original had single 6 and you edit to <code>6+6</code>, it will create two lines as expected.</div> <!-- Extra inputs --> <div style="margin-top:10px;display:flex;gap:8px;align-items:center"> <label style="font-size:13px">Extra (machine selected):</label> <input id="extra_date" type="date" class="input" /> <input id="extra_meter" class="input" placeholder="e.g. 12 or 12+25" /> <button id="btnAddExtra" class="btn ghost">Add Extra</button> </div> <div id="summaryWrap" style="margin-top:14px"></div> <div id="grandTotals" class="totalsBar" style="display:none"><div><strong>Grand total:</strong> <span id="grandTotalValue">0</span></div><div id="qualityLegend"></div></div> </div> </div> <!-- Shortcut tip box (top-right) --> <div id="shortcutTipBox" title="Keyboard shortcuts"> <strong>Shortcuts</strong> <div style="line-height:1.4;"> <div><kbd>Ctrl</kbd> + <kbd>Enter</kbd> → Machine</div> <div><kbd>Alt</kbd> + <kbd>Enter</kbd> → Load</div> <div><kbd>Shift</kbd> + <kbd>Enter</kbd> → Karigar select</div> <div><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Enter</kbd> → Add karigar</div> <div><kbd>Enter</kbd> on blank box → Accept & move next</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)); } } /* parse "12" or "12+25" => [12,25] */ function parseMetersRaw(raw){ if(typeof raw !== 'string') raw = String(raw||''); const parts = raw.split('+').map(s=>s.trim()).filter(s=>s!==''); const meters = parts.map(p=>{ const n=parseFloat(p); return isNaN(n)?null:n; }); if(meters.some(m=>m===null || m<=0)) return null; return meters; } /* init */ async function loadInit(){ const kh = $('khata'); kh.innerHTML = '<option>Loading…</option>'; try{ const j = await fetchJSON('?act=khata_list'); kh.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}` : ''); kh.appendChild(o); } }catch(e){ kh.innerHTML = '<option value="">(failed)</option>'; console.error(e); } 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(e); } } loadInit(); /* khata change -> machines + karigars */ $('khata').addEventListener('change', async (e)=>{ const opt = e.target.selectedOptions[0]; const ms = $('machine'); ms.innerHTML = '<option value="">—</option>'; $('extraKarigar').innerHTML = '<option value="">Select</option>'; if(!opt) return; const from = parseInt(opt.dataset.from||0) || 0, to = parseInt(opt.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(opt.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(e){ console.error(e); } }); /* machine change -> suggested quality */ $('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'); 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(e){ /*silent*/ } }); /* Add extra */ $('btnAddExtra').addEventListener('click', async ()=>{ const kh = $('khata').value.trim(); const machine = parseInt($('machine').value||0); const date = $('extra_date').value; const raw = $('extra_meter').value.trim(); const qid = parseInt($('quality_select').value||0); if(!kh||!machine) return alert('Select khata & machine'); if(!date) return alert('Select date'); if(!raw) return alert('Enter meter'); const meters = parseMetersRaw(raw); if(!meters) return alert('Invalid meter format'); try{ const resp = await fetchJSON('?act=add_extra', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ khata:kh, machine:machine, date:date, meters:meters, quality_id: qid }) }); alert('Extra saved: ' + (resp.action||'OK')); $('extra_date').value=''; $('extra_meter').value=''; $('btnLoad').click(); }catch(err){ alert('Add Extra failed: '+err.message); } }); /* Add karigar (client-side) */ $('btnAddKarigar').addEventListener('click', ()=>{ const sel = $('extraKarigar'); const vid = sel.value; if(!vid) return alert('Select karigar'); const text = 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) return alert('Load first'); const k = { karigar_id: vid, karigar_name: text, 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 first = card.querySelector('input.inlineMeter'); if(first){ first.focus(); first.select && first.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'); }; const left = document.createElement('div'); left.style.display='flex'; left.style.gap='10px'; left.style.alignItems='center'; left.appendChild(name); left.appendChild(totalSpan); head.appendChild(left); 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 ld = document.createElement('div'); ld.className='date'; ld.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 ? meters.map(m=>Math.round(m*100)/100).join('+') : (d.taka_display||''); const disp = document.createElement('div'); disp.className='taka'; disp.textContent = meterStr; 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') return; ev.preventDefault(); // current input and its value const target = ev.target; const raw = (target.value || '').toString().trim(); // if empty -> just focus next input (no alert / save) if (raw === '') { const currentRow = target.closest('.kg-row'); // move focus to next empty/available inlineMeter in same card focusNextDateInput(currentRow); return; } // otherwise proceed with normal create/save flow handleQuickAddEnter(target); }); right.appendChild(inp); } row.appendChild(ld); row.appendChild(right); datesWrap.appendChild(row); }); card.appendChild(datesWrap); 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.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'; const qsel = $('quality_select'); $('qualityLegend').textContent = qsel.value>0 ? ('Quality: '+(qsel.selectedOptions[0]?.textContent||'')) : ''; } /* render karigars view */ 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=>{ grid.appendChild(renderCardForKarigar(k)); }); wrap.appendChild(grid); document.querySelectorAll('.card-kg').forEach(c=>updateCardTotal(c)); } /* quick add from empty cell */ async function handleQuickAddEnter(inp){ const raw = (inp.value||'').trim(); if(!raw) return alert('Enter meter'); const meters = parseMetersRaw(raw); if(!meters) return alert('Invalid meter format'); 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('Select khata/machine'); 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); } 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); const card = row.closest('.card-kg'); updateCardTotal(card); focusNextDateInput(row); }catch(err){ alert('Add failed: '+err.message); inp.disabled = false; } } 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 ni = rows[i].querySelector('input.inlineMeter'); if(ni){ ni.focus(); ni.select && ni.select(); return; } } } /* Inline edit — updated to use text inputs (accept 12+25), Enter triggers save */ async function startEditInline(kid,date,triggerBtn){ const kh = $('khata').value.trim(); const machine = parseInt($('machine').value||0); if(!kh||!machine) return alert('Select khata & machine'); try{ const j = await fetchJSON(`?act=fetch_for_edit&khata=${encodeURIComponent(kh)}&machine=${machine}&date=${encodeURIComponent(date)}&karigar_id=${encodeURIComponent(kid)}`); const row = triggerBtn.closest('.kg-row'), right = row.querySelector('.smallActions'); right.innerHTML = ''; if(!j.data || !j.data.length){ right.innerHTML = '<div class="placeholder">No editable entries</div>'; return; } const inputsMeta = []; // {entry_id, karigar_id, date, el} // For each entry (taka) we render group with text inputs. If server has multiple lines for the karigar in same entry, we render multiple inputs. j.data.forEach(entry=>{ const group = document.createElement('div'); group.style.marginBottom='8px'; const lbl = document.createElement('div'); lbl.style.fontWeight='700'; lbl.textContent = `Taka #${entry.taka_no} (Entry ${entry.entry_id})`; group.appendChild(lbl); // allow per-entry quality selection (optional) - show current and allow change const qSel = document.createElement('select'); qSel.className='input small'; qSel.style.margin='6px 0 8px 0'; qSel.appendChild(new Option('(keep current)', '')); (window.__qualityList || []).forEach(q=>{ const o = document.createElement('option'); o.value=q.quality_id; o.textContent=q.quality_name; if(entry.quality_id && Number(q.quality_id)===Number(entry.quality_id)) o.selected=true; qSel.appendChild(o); }); group.appendChild(qSel); // render existing lines as text inputs (allow editing to 12+25) entry.lines.forEach(ln=>{ const r = document.createElement('div'); r.style.display='flex'; r.style.gap='6px'; r.style.alignItems='center'; r.style.marginBottom='6px'; const inpt = document.createElement('input'); inpt.type='text'; inpt.className='inlineMeter'; inpt.value = (Math.round((ln.meter||0)*100)/100).toString(); inpt.placeholder = '12 or 12+25'; // Enter triggers save inpt.addEventListener('keydown', (ev)=>{ if(ev.key==='Enter'){ ev.preventDefault(); saveBtn && saveBtn.click(); } }); r.appendChild(inpt); group.appendChild(r); inputsMeta.push({ entry_id: entry.entry_id, karigar_id: ln.karigar_id, date: ln.date, el: inpt, qsel: qSel, entry_quality_id: entry.quality_id }); }); // allow user to add additional blank input in edit modal for this entry const addLineBtn = document.createElement('button'); addLineBtn.className='btn ghost'; addLineBtn.textContent='Add line'; addLineBtn.onclick = ()=>{ const rr = document.createElement('div'); rr.style.display='flex'; rr.style.gap='6px'; rr.style.alignItems='center'; rr.style.marginBottom='6px'; const newIn = document.createElement('input'); newIn.type='text'; newIn.className='inlineMeter'; newIn.placeholder='12 or 12+25'; newIn.addEventListener('keydown', (ev)=>{ if(ev.key==='Enter'){ ev.preventDefault(); saveBtn && saveBtn.click(); } }); rr.appendChild(newIn); group.insertBefore(rr, addLineBtn); inputsMeta.push({ entry_id: entry.entry_id, karigar_id: kid, date: entry.lines[0]?.date || date, el: newIn, qsel:qSel, entry_quality_id: entry.quality_id }); newIn.focus(); }; group.appendChild(addLineBtn); 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 ()=>{ // Build edits payload: for each entry_id collect all values (support +) const map = {}; const qualityForEntry = {}; for(const meta of inputsMeta){ const raw = (meta.el.value||'').toString().trim(); if(raw==='' || meta.entry_id<=0) continue; // Supports '12+25' inside one input: explode into multiple numeric lines const parts = raw.split('+').map(s=>s.trim()).filter(s=>s!==''); for(const p of parts){ const val = parseFloat(p); if(isNaN(val) || val<=0) continue; if(!map[meta.entry_id]) map[meta.entry_id] = []; map[meta.entry_id].push({ karigar_id: meta.karigar_id, meter: val, date: (meta.date||date).substr(0,10) }); } // quality per entry (explicit) const qv = meta.qsel && meta.qsel.value !== '' ? parseInt(meta.qsel.value||0,10) : undefined; if(typeof qv !== 'undefined') qualityForEntry[meta.entry_id] = qv; } const edits = Object.keys(map).map(eid=>{ const obj = { entry_id: parseInt(eid,10), lines: map[eid] }; if(typeof qualityForEntry[eid] !== 'undefined') obj.quality_id = qualityForEntry[eid]; return obj; }); 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 view (prefer incremental but safe to reload) await sleep(200); $('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); } } // small helper function sleep(ms){ return new Promise(resolve=>setTimeout(resolve,ms)); } /* Load summary */ $('btnLoad').addEventListener('click', async ()=>{ const kh = $('khata').value.trim(); const m = parseInt($('machine').value||0); const mo = $('month').value; // period select removed; default to H1 const per = 'H1'; if(!kh||!m||!mo) return alert('Fill khata/machine/month'); const wrap = $('summaryWrap'); wrap.innerHTML = '<div class="placeholder">Loading…</div>'; const b = $('btnLoad'); b.disabled=true; b.textContent='Loading…'; try{ const j = await fetchJSON(`?act=fetch_summary&khata=${encodeURIComponent(kh)}&machine=${m}&month=${encodeURIComponent(mo)}&period=${encodeURIComponent(per)}`); renderKarigars(j); 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); }); window.__qualityList = window.__qualityList || (await (async()=>{ const q=await fetchJSON('?act=quality_list'); return q.qualities||[]; })()); updateGrandTotal(); }catch(err){ wrap.innerHTML = `<div class="placeholder" style="color:#b91c1c">Error: ${err.message}</div>`; } finally{ b.disabled=false; b.textContent='Load'; } }); /* Clear */ $('btnClear').addEventListener('click', ()=>{ $('khata').value=''; $('machine').innerHTML='<option value="">—</option>'; $('month').value=''; $('summaryWrap').innerHTML=''; $('grandTotals').style.display='none'; $('khata').focus(); }); /* --- Keyboard Shortcuts (working) --- */ document.addEventListener('keydown', function(e){ // Ctrl+Enter → Machine selector if(e.key === "Enter" && e.ctrlKey && !e.shiftKey && !e.altKey){ e.preventDefault(); const m = document.getElementById('machine'); if(m){ m.focus(); if(typeof m.select === 'function') try{ m.select(); }catch(_){} } return; } // Alt+Enter → Load if(e.key === "Enter" && e.altKey && !e.ctrlKey && !e.shiftKey){ e.preventDefault(); const b = document.getElementById('btnLoad'); if(b) b.click(); return; } // Shift+Enter → New karigar selector drop down focus if(e.key === "Enter" && e.shiftKey && !e.ctrlKey && !e.altKey){ e.preventDefault(); const ek = document.getElementById('extraKarigar'); if(ek){ ek.focus(); if(typeof ek.open === 'function') try{ ek.open(); }catch(_){} } return; } // Ctrl+Shift+Enter → Add karigar if(e.key === "Enter" && e.ctrlKey && e.shiftKey){ e.preventDefault(); const ab = document.getElementById('btnAddKarigar'); if(ab) ab.click(); return; } }); </script> </body></html>