« Back to History
machine_wise_production.php
|
20260721_154032.php
Initial Bulk Import
Copy Code
<?php /* machine_wise_production.php Machine summary — Inline Add/Edit, Add Karigar, Add Extra, assigned-only karigars, append same-date entries, per-line karigar dropdown in Edit. Backup your original 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; } /* DB bootstrap */ $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_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); } 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; } /* 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_list': { $list = quality_list($pdo,$COMPANY_ID); jexit(['ok'=>true,'qualities'=>$list]); } case 'quality_by_machine': { $m=(int)($_GET['machine_no']??0); if($m<=0) jexit(['ok'=>false,'msg'=>'Missing machine_no'],400); $res = quality_by_machine($pdo,$COMPANY_ID,$m); if($res) jexit(['ok'=>true,'quality'=>$res]); jexit(['ok'=>false,'msg'=>'Quality not found'],404); } /* add_karigar: create new karigar assigned to this khata and machine */ case 'add_karigar': { $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); $name = trim($in['name'] ?? ''); if(!$kcode || !$machine || $name==='') 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); try{ $ins = $pdo->prepare("INSERT INTO loom_karigar_master (company_id,khata_id,karigar_name,machine_from,machine_to,is_active,created_by,created_at) VALUES (?,?,?,?,?,?,?,NOW())"); $ins->execute([$COMPANY_ID,(int)$kh['id'],$name,$machine,$machine,1,$USER_ID]); jexit(['ok'=>true,'karigar'=>['id'=>$pdo->lastInsertId(),'name'=>$name]]); } catch(Throwable $e){ jexit(['ok'=>false,'msg'=>'Insert failed','detail'=>$e->getMessage()],500); } } /* fetch_summary: ONLY assigned karigars for this machine */ 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 = []; 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); $mtr = floatval($ln['meter'] ?? 0); if($kid<=0) continue; 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'=>$mtr,'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) + $mtr; } } // 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'); // ONLY assigned karigars $assigned = fetch_assigned_karigars($pdo,$COMPANY_ID,$kh['id'],$machine); $all_kar = fetch_all_karigars_in_khata($pdo,$COMPANY_ID,$kh['id']); $out = []; foreach($assigned as $k){ $kid = (int)$k['id']; $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]; } $assigned_out = []; foreach($assigned as $k) $assigned_out[]=['id'=>$k['id'],'name'=>$k['karigar_name']]; $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,'assigned_karigars'=>$assigned_out,'all_karigars'=>$kar_map]); } /* fetch_for_edit: load entries for a date & karigar */ 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]); } /* create_line: handles single meter or meters[]; default append=true (same-date) */ 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); $quality_id = isset($in['quality_id']) ? (int)$in['quality_id'] : 0; $append = isset($in['append']) ? (bool)$in['append'] : true; $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(!$kcode || !$machine || !$date || $kid<=0 || empty($meters)) 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 existing same-date entry (latest) for append $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)){ if($append){ $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'=>$kid,'meter'=>round($mval,2),'date'=>substr($date,0,10)]; } $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','entry_id'=>$entry_id,'taka_no'=>$row['taka_no'],'meter_added'=>array_sum($meters),'meter_total'=>$new_total]); } } // create new taka (no existing or append==false) $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'=>$kid,'meter'=>round($mval,2),'date'=>substr($date,0,10)]; $sum += floatval($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','entry_id'=>$nid,'taka_no'=>$next,'meter_total'=>$sum,'quality_id'=>$quality_id]); } catch(Throwable $e){ $pdo->rollBack(); jexit(['ok'=>false,'msg'=>'create failed','detail'=>$e->getMessage()],500); } } /* add_extra: append a line with karigar_id=0 and optional note */ 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']??''); $metersArr = []; if(isset($in['meters']) && is_array($in['meters'])){ foreach($in['meters'] as $m){ $m2=floatval($m); if($m2>0) $metersArr[] = round($m2,2); } } elseif(isset($in['meter'])){ $m2=floatval($in['meter']); if($m2>0) $metersArr[] = round($m2,2); } $note = trim($in['note'] ?? ''); $quality_id = (int)($in['quality_id'] ?? 0); if(!$kcode||!$machine||!$date||empty($metersArr)) 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(); $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($metersArr 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($metersArr),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_total'=>$new_total]); } else { $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; foreach($metersArr as $mval){ $lines[]=['karigar_id'=>0,'meter'=>round($mval,2),'date'=>substr($date,0,10),'extra_note'=>$note]; $sum += $mval; } $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),round($sum,2),$USER_ID]); $nid = $pdo->lastInsertId(); $pdo->commit(); jexit(['ok'=>true,'action'=>'created_extra','entry_id'=>$nid,'taka_no'=>$next,'meter_total'=>round($sum,2)]); } } catch(Throwable $e){ $pdo->rollBack(); jexit(['ok'=>false,'msg'=>'add_extra failed','detail'=>$e->getMessage()],500); } } /* update_entries: replace relevant lines in entry.lines_json; apply quality_id only if provided (explicit) */ 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=[]; // we'll replace lines for the (karigar_id,date) pairs provided $to_replace = []; foreach($e['lines'] as $ln){ $kd = (int)($ln['karigar_id'] ?? -1); $dd = substr(trim($ln['date'] ?? ''),0,10); if($kd>=0 && $dd!=='') $to_replace[] = $kd.'::'.$dd; } $new_lines = []; // remove old lines matching the keys 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; } // now add new lines (may include changed karigar_id) foreach($e['lines'] as $ln){ $k = (int)($ln['karigar_id'] ?? -1); $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); $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(isset($e['quality_id'])){ $new_q = (int)$e['quality_id']; $qupd = $pdo->prepare("UPDATE loom_production_entry SET quality_id=? WHERE id=? AND company_id=?"); $qupd->execute([$new_q, $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 Summary — Inline Add/Edit</title> <style> :root{ --brand:#1d4ed8; --muted:#6b7280; --line:#e6e9ee; } 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 var(--line);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:var(--brand);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 var(--line);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:var(--muted);padding:12px} .totalsBar{margin-top:12px;padding:10px;background:#fff;border:1px solid var(--line);border-radius:8px;display:flex;justify-content:space-between;align-items:center} .extraBox{display:flex;gap:8px;align-items:center} .mini{padding:6px 8px;font-size:13px;border-radius:6px} @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 (for new entries)</label><br> <select id="quality_select" class="input small"><option value="0">(choose)</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> <!-- Add Karigar (NEW) --> <div style="display:flex;gap:8px;align-items:center"> <input id="new_karigar_name" class="input mini" placeholder="New karigar name" /> <button id="btnAddKarigar" class="btn ghost">Add Karigar</button> </div> <div style="margin-left:auto" class="extraBox"> <select id="extra_machine" class="input small"><option value="">M</option></select> <input id="extra_date" type="date" class="input" style="width:150px" /> <input id="extra_meter" class="input" placeholder="mtr e.g. 12 or 12+25" style="width:160px" /> <input id="extra_note" class="input" placeholder="note (optional)" style="width:160px" /> <button id="btnAddExtra" class="btn ghost">Add Extra</button> </div> </div> <div style="margin-top:8px;color:#6b7280;font-size:13px"> Tip: Enter numbers like <code>12</code> or <code>12+25</code>. For new entries top Quality will be used. Edit existing entry → change meters or quality or karigar → press <strong>Enter</strong> (on input) or click Save. </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> /* --- helper fetch + utils --- */ 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)); } } function parseMetersRaw(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: khatas + qualities --- */ 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>'; j.khatas.forEach(khata=>{ const o = document.createElement('option'); o.value = khata.code; o.dataset.kid = khata.id; o.dataset.from = khata.machine_from; o.dataset.to = khata.machine_to; o.textContent = khata.code + (khata.machine_from && khata.machine_to ? ` — ${khata.machine_from}-${khata.machine_to}` : ''); kh.appendChild(o); }); }catch(e){ kh.innerHTML = '<option value="">(khata load failed)</option>'; console.error(e); } try{ const q = await fetchJSON('?act=quality_list'); window.__qualityList = q.qualities || []; const sel = $('quality_select'); sel.innerHTML = '<option value="0">(choose)</option>'; (window.__qualityList||[]).forEach(qi=>{ const o=document.createElement('option'); o.value=qi.quality_id; o.textContent=qi.quality_name; sel.appendChild(o); }); }catch(e){ console.error('quality_list failed',e); } // prepare extra_machine placeholder $('extra_machine').innerHTML = '<option value="">M</option>'; } loadInit(); /* when khata selected — populate machines and extra_machine */ $('khata').addEventListener('change', (e)=>{ const opt = e.target.selectedOptions[0]; const ms = $('machine'); ms.innerHTML = '<option value="">—</option>'; const em = $('extra_machine'); em.innerHTML = '<option value="">M</option>'; $('extra_date').value=''; if(!opt) return; const from = parseInt(opt.dataset.from||0)||0; const 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); const o2 = o.cloneNode(true); em.appendChild(o2); } } }); /* machine change -> suggest quality_by_machine */ $('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 Karigar */ $('btnAddKarigar').addEventListener('click', async ()=>{ const kh = $('khata').value.trim(); const machine = parseInt($('machine').value||0); const name = $('new_karigar_name').value.trim(); if(!kh || !machine || !name) return alert('Fill Khata, Machine and Name to add karigar'); try{ const resp = await fetchJSON('?act=add_karigar', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ khata:kh, machine:machine, name:name }) }); alert('Karigar added: ' + (resp.karigar.name || 'OK')); $('new_karigar_name').value=''; // reload summary so new karigar shows up $('btnLoad').click(); }catch(err){ alert('Add Karigar failed: '+err.message); } }); /* 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)}`); // cache assigned karigars for edit dropdowns window.__assignedKarigars = j.assigned_karigars || []; window.__allKarigars = j.all_karigars || []; renderKarigars(j); window.__lastSummaryData = j; updateGrandTotal(); }catch(err){ wrap.innerHTML = `<div class="placeholder" style="color:#b91c1c">Error: ${err.message}</div>`; } finally { btn.disabled=false; btn.textContent='Load'; } }); /* Add Extra */ $('btnAddExtra').addEventListener('click', async ()=>{ const kh = $('khata').value.trim(); const machine = parseInt($('extra_machine').value||0); const date = $('extra_date').value; const raw = $('extra_meter').value.trim(); const note = $('extra_note').value.trim(); const qid = parseInt($('quality_select').value||0); if(!kh||!machine||!date||!raw) return alert('Fill khata, machine, date and meter'); const meters = parseMetersRaw(raw); if(!meters) return alert('Invalid meter format (use 12 or 12+25)'); 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, note:note, quality_id: qid }) }); alert('Extra added: ' + (resp.action || 'OK')); $('btnLoad').click(); }catch(err){ alert('Add Extra failed: '+err.message); } }); /* rendering 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 ? (d.taka_display||'') : 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); 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||'')) : ''; } 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); document.querySelectorAll('.card-kg').forEach(c=>updateCardTotal(c)); } /* Quick add inline (Enter) */ async function handleQuickAddEnter(inp){ const raw = (inp.value||'').trim(); if(!raw) return alert('Enter meter > 0'); const meters = parseMetersRaw(raw); if(!meters) return alert('Invalid meter format (use 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'); inp.disabled=true; try{ const resp = await fetchJSON('?act=create_line', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ khata:kh, machine:m, date:date, karigar_id:kid, meters:meters, quality_id: qid, append:true })}); const row = inp.closest('.kg-row'); const right = row.querySelector('.smallActions'); const meterStr = meters.map(x=>Math.round(x*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 nextInp = rows[i].querySelector('input.inlineMeter'); if(nextInp){ nextInp.focus(); nextInp.select && nextInp.select(); return; } } } /* Inline edit with Enter-to-save and karigar-select per-line */ 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 qlist = window.__qualityList || []; const assigned = window.__assignedKarigars || []; // [{id,name},...] const inputsMeta = []; 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); // quality select for this entry (explicit) const qSel = document.createElement('select'); qSel.className='input small'; qSel.style.margin='6px 0 8px 0'; const keepOpt = document.createElement('option'); keepOpt.value=''; keepOpt.textContent='(keep current)'; qSel.appendChild(keepOpt); qlist.forEach(qi=>{ const o=document.createElement('option'); o.value=qi.quality_id; o.textContent=qi.quality_name; if(entry.quality_id && Number(qi.quality_id)===Number(entry.quality_id)) o.selected=true; qSel.appendChild(o); }); if(entry.quality_id && !Array.from(qSel.options).some(o=>Number(o.value)===Number(entry.quality_id))){ const optx=document.createElement('option'); optx.value=entry.quality_id; optx.textContent='Q#'+entry.quality_id; optx.selected=true; qSel.appendChild(optx); } group.appendChild(qSel); // for each line allow changing karigar (dropdown) and meter edit entry.lines.forEach(ln=>{ const r = document.createElement('div'); r.style.display='flex'; r.style.gap='8px'; r.style.alignItems='center'; r.style.marginBottom='6px'; // karigar select (assigned list) const ksel = document.createElement('select'); ksel.className='input small'; (assigned||[]).forEach(a=>{ const o=document.createElement('option'); o.value = a.id; o.textContent = a.name; if(Number(a.id) === Number(ln.karigar_id)) o.selected = true; ksel.appendChild(o); }); // fallback: if current karigar not in assigned, add it if(ln.karigar_id && !Array.from(ksel.options).some(o=>Number(o.value)===Number(ln.karigar_id))){ const ox = document.createElement('option'); ox.value = ln.karigar_id; ox.textContent='K#'+ln.karigar_id; ox.selected=true; ksel.appendChild(ox); } // meter input const inpt = document.createElement('input'); inpt.type='number'; inpt.step='0.01'; inpt.className='inlineMeter'; inpt.value = ln.meter; inpt.style.width='120px'; // Enter on this input -> save inpt.addEventListener('keydown',(ev)=>{ if(ev.key==='Enter'){ ev.preventDefault(); saveBtn && saveBtn.click(); } }); inputsMeta.push({ entry_id: entry.entry_id, karigar_select: ksel, karigar_id: ln.karigar_id, date: ln.date, el: inpt, qsel: qSel, entry_quality_id: entry.quality_id }); r.appendChild(ksel); 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 = {}; const qualityForEntry = {}; inputsMeta.forEach(meta=>{ const val = parseFloat(meta.el.value||0) || 0; const selectedKarigar = parseInt(meta.karigar_select.value||0) || 0; if(!map[meta.entry_id]) map[meta.entry_id] = []; map[meta.entry_id].push({ karigar_id: selectedKarigar, meter: val, date: meta.date }); const qv = meta.qsel.value; if(qv !== '') qualityForEntry[meta.entry_id] = parseInt(qv,10) || 0; }); 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