« Back to History
production_entry_new.php
|
20260723_000646.php
Initial Domain Snapshot
Copy Code
<?php /* ============================================================================= File: /erp/loom_production_entry.php Purpose: Loom production entry (machine) - UI patched: duplicate karigar total at bottom, removed Tip text, page-level option to hide header/menu Updated: 2025-12-09 (UI patch) ============================================================================= */ 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']; /* If you want header/menu hidden for this page, set this flag. header.php can check $PAGE_HIDE_TOPBAR and avoid rendering the top menu. As a fallback, this file includes a small JS to hide common header selectors. */ $PAGE_HIDE_TOPBAR = true; 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); /* ----- Module config ----- */ $ENTRY_TABLE = 'production_entry'; /* ----- 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); } /* === BEAM START === Beam tracking helper functions === BEAM END === */ function get_beams_for_machine($pdo, $company_id, $machine_no){ $stmt = $pdo->prepare("SELECT primary_running_beam_no, secondary_running_beam_no FROM beam_mapping WHERE company_id = ? AND machine_no = ? LIMIT 1"); $stmt->execute([$company_id, $machine_no]); return $stmt->fetch(PDO::FETCH_ASSOC); } function validate_beam_meters($pdo, $company_id, $beam_no, $meter_change){ if ($beam_no === null || $beam_no === '') return ['ok'=>true]; $stmt = $pdo->prepare("SELECT meter_pending FROM beam_book WHERE company_id = ? AND beam_no = ? LIMIT 1"); $stmt->execute([$company_id, $beam_no]); $beam = $stmt->fetch(PDO::FETCH_ASSOC); if (!$beam) return ['ok'=>false,'msg'=>"Beam $beam_no not found"]; $pending = (float)$beam['meter_pending']; if ($pending < $meter_change) return ['ok'=>false,'msg'=>"Beam $beam_no insufficient. Available: $pending, Required: $meter_change"]; return ['ok'=>true]; } function update_beam_meters($pdo, $company_id, $beam_no, $meter_change){ if ($beam_no === null || $beam_no === '') return true; $stmt = $pdo->prepare("UPDATE beam_book SET meter_used = meter_used + ?, meter_pending = meter_pending - ? WHERE company_id = ? AND beam_no = ?"); return $stmt->execute([$meter_change,$meter_change,$company_id,$beam_no]); } function rollback_beam_meters($pdo, $company_id, $beam_no, $meter_change){ if ($beam_no === null || $beam_no === '') return true; $stmt = $pdo->prepare("UPDATE beam_book SET meter_used = meter_used - ?, meter_pending = meter_pending + ? WHERE company_id = ? AND beam_no = ?"); return $stmt->execute([$meter_change,$meter_change,$company_id,$beam_no]); } function handle_beam_updates($pdo, $company_id, $machine_no, $meter_change, $operation='deduct'){ $beams = get_beams_for_machine($pdo,$company_id,$machine_no); if (!$beams) return ['ok'=>true]; $pbn = $beams['primary_running_beam_no']; $sbn = $beams['secondary_running_beam_no']; if ($operation==='deduct'){ $pchk = validate_beam_meters($pdo,$company_id,$pbn,$meter_change); if (!$pchk['ok']) return $pchk; $schk = validate_beam_meters($pdo,$company_id,$sbn,$meter_change); if (!$schk['ok']) return $schk; $pok = update_beam_meters($pdo,$company_id,$pbn,$meter_change); $sok = update_beam_meters($pdo,$company_id,$sbn,$meter_change); if (!$pok||!$sok) return ['ok'=>false,'msg'=>'Failed to update beam meters']; } else { // rollback $pok = rollback_beam_meters($pdo,$company_id,$pbn,$meter_change); $sok = rollback_beam_meters($pdo,$company_id,$sbn,$meter_change); if (!$pok||!$sok) return ['ok'=>false,'msg'=>'Failed to rollback beam meters']; } return ['ok'=>true]; } /* ----- AJAX router (unchanged endpoints) ----- */ $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 {$ENTRY_TABLE} 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 {$ENTRY_TABLE} 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(); // === BEAM START === $beam_check = handle_beam_updates($pdo,$COMPANY_ID,$machine,$meter,'deduct'); if (!$beam_check['ok']) { $pdo->rollBack(); jexit(['ok'=>false,'msg'=>$beam_check['msg']],400); } // === BEAM END === // >>> override quality from loom_quality_master if not provided $suggest = quality_by_machine($pdo,$COMPANY_ID,$machine); if ($quality_id <= 0 && is_array($suggest) && !empty($suggest['quality_id'])) { $quality_id = (int)$suggest['quality_id']; } $s = $pdo->prepare("SELECT COALESCE(MAX(taka_no),0) FROM {$ENTRY_TABLE} 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 {$ENTRY_TABLE} (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(); // === BEAM START === $total_meter_change = array_sum($meters); $beam_check = handle_beam_updates($pdo,$COMPANY_ID,$machine,$total_meter_change,'deduct'); if (!$beam_check['ok']) { $pdo->rollBack(); jexit(['ok'=>false,'msg'=>$beam_check['msg']],400); } // === BEAM END === // >>> override quality from loom_quality_master if not provided $suggest = quality_by_machine($pdo,$COMPANY_ID,$machine); if ($quality_id <= 0 && is_array($suggest) && !empty($suggest['quality_id'])) { $quality_id = (int)$suggest['quality_id']; } $sel = $pdo->prepare("SELECT id,lines_json,meter_total,taka_no FROM {$ENTRY_TABLE} 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 {$ENTRY_TABLE} 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 {$ENTRY_TABLE} 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 {$ENTRY_TABLE} 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 {$ENTRY_TABLE} (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': { $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, machine_id FROM {$ENTRY_TABLE} WHERE company_id=? AND id=? FOR UPDATE"); $st->execute([$COMPANY_ID,$entry_id]); $row=$st->fetch(PDO::FETCH_ASSOC); if(!$row) continue; $machine_id = (int)$row['machine_id']; $orig = json_decode($row['lines_json'] ?? '[]', true); if(!is_array($orig)) $orig=[]; // === BEAM START === $old_total = 0.0; foreach($orig as $line){ $old_total += (float)($line['meter'] ?? 0); } $to_replace=[]; $incoming_lines = $e['lines'] ?? []; $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(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; } $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; } 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)]; } $new_total = 0.0; foreach($new_lines as $nl) $new_total += floatval($nl['meter'] ?? 0); $new_total = round($new_total,2); $meter_delta = $new_total - $old_total; if ($meter_delta != 0) { $beam_check = handle_beam_updates($pdo,$COMPANY_ID,$machine_id,abs($meter_delta), $meter_delta>0 ? 'deduct' : 'rollback'); if (!$beam_check['ok']) { $pdo->rollBack(); jexit(['ok'=>false,'msg'=>$beam_check['msg']],400); } } // === BEAM END === $upd = $pdo->prepare("UPDATE {$ENTRY_TABLE} SET lines_json=?, meter_total=? WHERE id=? AND company_id=?"); $upd->execute([json_encode($new_lines, JSON_UNESCAPED_UNICODE), $new_total, $entry_id, $COMPANY_ID]); if(isset($e['quality_id'])){ $qv = (int)$e['quality_id']; $qup = $pdo->prepare("UPDATE {$ENTRY_TABLE} SET quality_id=? WHERE id=? AND company_id=?"); $qup->execute([$qv,$entry_id,$COMPANY_ID]); } $updated[] = ['entry_id'=>$entry_id,'meter_total'=>$new_total]; } $pdo->commit(); jexit(['ok'=>true,'updated'=>$updated]); } catch(Throwable $e){ $pdo->rollBack(); jexit(['ok'=>false,'msg'=>'Update failed','detail'=>$e->getMessage()],500); } } case 'delete_entry': { $in = json_decode(file_get_contents('php://input'), true); if(!$in || empty($in['entry_id'])) jexit(['ok'=>false,'msg'=>'Missing entry_id'],400); $eid = (int)$in['entry_id']; try{ $pdo->beginTransaction(); $st = $pdo->prepare("SELECT id, machine_id, meter_total FROM {$ENTRY_TABLE} WHERE company_id=? AND id=? LIMIT 1 FOR UPDATE"); $st->execute([$COMPANY_ID,$eid]); $entry = $st->fetch(PDO::FETCH_ASSOC); if(!$entry){ $pdo->rollBack(); jexit(['ok'=>false,'msg'=>'Entry not found'],404); } $machine_id = (int)$entry['machine_id']; $meter_total = (float)$entry['meter_total']; // === BEAM START === if ($meter_total > 0) { $beam_check = handle_beam_updates($pdo,$COMPANY_ID,$machine_id,$meter_total,'rollback'); if (!$beam_check['ok']) { $pdo->rollBack(); jexit(['ok'=>false,'msg'=>$beam_check['msg']],400); } } // === BEAM END === $del = $pdo->prepare("DELETE FROM {$ENTRY_TABLE} WHERE id=? AND company_id=?"); $del->execute([$eid,$COMPANY_ID]); $pdo->commit(); jexit(['ok'=>true,'deleted_id'=>$eid,'rows'=>$del->rowCount()]); }catch(Throwable $e){ $pdo->rollBack(); jexit(['ok'=>false,'msg'=>'Delete failed','detail'=>$e->getMessage()],500); } } case 'save_period': { $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); $from = trim($in['from'] ?? ''); $to = trim($in['to'] ?? ''); $quality_id = isset($in['quality_id']) ? (int)$in['quality_id'] : 0; $lines = $in['aggregated_lines'] ?? null; $mode = $in['mode'] ?? 'insert'; if(!$kcode || !$machine || !$from || !$to || !is_array($lines) || empty($lines)) 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{ $pdo->beginTransaction(); // === BEAM START === $meter_sum = 0.0; foreach($lines as $ln){ $meter_sum += floatval($ln['meter'] ?? 0); } if ($meter_sum > 0) { $beam_check = handle_beam_updates($pdo,$COMPANY_ID,$machine,$meter_sum,'deduct'); if (!$beam_check['ok']) { $pdo->rollBack(); jexit(['ok'=>false,'msg'=>$beam_check['msg']],400); } } // === BEAM END === // >>> override quality from loom_quality_master if not provided $suggest = quality_by_machine($pdo,$COMPANY_ID,$machine); if ($quality_id <= 0 && is_array($suggest) && !empty($suggest['quality_id'])) { $quality_id = (int)$suggest['quality_id']; } $s = $pdo->prepare("SELECT COALESCE(MAX(taka_no),0) FROM {$ENTRY_TABLE} WHERE company_id=? AND khata_id=? FOR UPDATE"); $s->execute([$COMPANY_ID,(int)$kh['id']]); $last=(int)$s->fetchColumn(); $next=$last+1; $json_lines = json_encode($lines, JSON_UNESCAPED_UNICODE); $ins = $pdo->prepare("INSERT INTO {$ENTRY_TABLE} (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,$from,$machine,(int)$kh['id'],$quality_id,$next,$json_lines,round($meter_sum,2),$USER_ID]); $new_id = $pdo->lastInsertId(); if($mode === 'move'){ $del = $pdo->prepare("DELETE FROM {$ENTRY_TABLE} WHERE company_id=? AND khata_id=? AND machine_id=? AND entry_date BETWEEN ? AND ?"); $del->execute([$COMPANY_ID,(int)$kh['id'],$machine,$from,$to]); } $pdo->commit(); jexit(['ok'=>true,'action'=>'saved_period','entry_id'=>$new_id,'taka_no'=>$next,'meter_total'=>round($meter_sum,2)]); }catch(Throwable $e){ $pdo->rollBack(); jexit(['ok'=>false,'msg'=>'save_period 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) ----- */ /* robust header include with fallbacks to avoid fatal require issues */ $__header_paths = [ __DIR__ . '/partials/header.php', __DIR__ . '/erp/partials/header.php', __DIR__ . '/../erp/partials/header.php', __DIR__ . '/../../erp/partials/header.php', $_SERVER['DOCUMENT_ROOT'] . '/erp/partials/header.php', ]; $__header_included = false; foreach ($__header_paths as $__hp) { if ($__hp && file_exists($__hp)) { require_once $__hp; $__header_included = true; break; } } if (! $__header_included) { error_log('loom_production_entry.php: header.php not found. Tried: ' . implode(' | ', $__header_paths)); echo "<!-- WARNING: header.php not found. checked: " . htmlentities(implode(', ', $__header_paths)) . " -->\n"; echo "<div style=\"padding:8px;background:#fff4e5;border:1px solid #ffddb5;margin:8px;border-radius:6px;color:#92400e;font-size:13px\">Header include not found. Check ERP partials path.</div>\n"; } ?><!doctype html> <html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Loom Production Entry</title> <style> /* local page styling (keeps layout isolated) */ body{font-family:Inter,system-ui,Arial;margin:12px;background:#f8fafc;color:#0f172a} .container{width:98%;max-width:none;margin:6px auto;padding:14px} .card{background:#fff;padding:16px;border:1px solid #e6e9ee;border-radius:10px;box-shadow:0 2px 6px rgba(15,23,42,0.02)} .row{display:flex;gap:12px;align-items:end;flex-wrap:wrap} .input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #e2e8f0;font-size:14px;background:#fff} .input.small{width:120px}.input.full{min-width:220px;width:240px} .btn{background:#1f6feb;color:#fff;border:none;padding:8px 12px;border-radius:8px;cursor:pointer;box-shadow:0 2px 6px rgba(31,111,235,0.08)} .btn.ghost{background:#fff;color:#111;border:1px solid #e2e8f0} .grid{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px} .card-kg{background:#fff;border:1px solid #eef2ff;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} .inlineMeter:focus{outline:2px solid rgba(31,111,235,0.25)} .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} .navMachine{display:flex;align-items:center;gap:6px;padding:6px;border-radius:8px;border:1px solid #e6e9ee;background:#fff} .navMachine button{border:none;background:transparent;padding:6px;border-radius:6px;cursor:pointer} .navMachine .machineBox{min-width:70px;text-align:center;padding:6px 10px;border-radius:8px;border:1px solid #eef2ff;background:#f8fafc} .controlsTop{margin-left:auto;display:flex;gap:8px;align-items:center} /* Flip visual: when .flipColumns on grid -> reverse order */ .grid.flipColumns{flex-direction:row-reverse} @media (max-width:920px){ .grid{flex-direction:column} .card-kg{max-width:100%} } /* footer duplicate total */ .kg-footer{margin-top:8px;border-top:1px dashed #f1f5f9;padding-top:8px;display:flex;justify-content:space-between;align-items:center} .kg-footer .kg-total{font-size:13px;color:#0f172a;background:#f1f5f9;padding:6px 10px;border-radius:8px} /* Shortcuts box (static at bottom of card) */ .shortcutsBox{margin-top:18px;padding:14px;border-radius:8px;background:#fff;border:1px solid #eef2ff;color:#374151} kbd{background:#f3f4f6;border-radius:4px;padding:3px 6px;border:1px solid #e6e7eb;font-weight:700;margin-right:6px} </style> </head><body> <div class="container"> <!-- top quick-nav intentionally removed for this page --> <div class="card"> <h3 style="margin:0 0 10px 0">Loom Production Entry — Machine</h3> <div class="row" style="align-items:center"> <div style="min-width:260px"><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</option><option value="H2">H2</option></select></div> <div style="display:flex;gap:6px;align-items:center"> <button id="btnLoad" class="btn">Load →</button> <div class="navMachine" title="Previous / current / Next"> <button id="btnPrevMachine" title="Previous machine">‹</button> <div class="machineBox" id="machineDisplay">—</div> <button id="btnNextMachine" title="Next machine">›</button> </div> <button id="btnClear" class="btn ghost">Clear</button> </div> <div class="controlsTop"> <div style="display:flex;flex-direction:column;align-items:flex-end"> <label style="font-size:12px"> </label> <button id="btnFlip" class="btn ghost">Flip order</button> </div> <div style="display:flex;flex-direction:column;align-items:flex-end"> <label style="font-size:12px">Add karigar:</label> <div><select id="extraKarigar" class="input small"><option value="">Select</option></select><button id="btnAddKarigar" class="btn ghost">Add</button></div> </div> <div style="display:flex;flex-direction:column;align-items:flex-end"> <label style="font-size:12px"> </label> <button id="btnSavePeriod" class="btn">Save Period (aggregate)</button> </div> </div> </div> <!-- Tip removed --> <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 class="shortcutsBox" id="shortcutsBox"> <strong>Shortcuts</strong> <div style="margin-top:8px;line-height:1.6"> <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>Delete</kbd> → Delete selected entry (when editing / focus within edit)</div> <div><kbd>Enter</kbd> on blank box → Accept & move next</div> </div> </div> </div> </div> <script> const $ = id => document.getElementById(id); /* improved fetchJSON */ async function fetchJSON(url, opt){ const resp = await fetch(url, opt); const text = await resp.text(); const ctype = (resp.headers.get('content-type') || '').toLowerCase(); if(ctype.indexOf('application/json') === -1){ const preview = text.slice(0,800); console.error('Non-JSON response for', url, 'status=', resp.status, 'content-type=', ctype, preview); const lowered = preview.toLowerCase(); if(lowered.indexOf('<form') !== -1 && (lowered.indexOf('login') !== -1 || lowered.indexOf('username') !== -1 || lowered.indexOf('password') !== -1)){ throw new Error('Session expired or not authenticated — server returned HTML login page. Try reloading the page or logging in again.'); } if(resp.status >= 500){ throw new Error('Server error: ' + resp.status + ' — see console for response preview.'); } throw new Error('Invalid server response (not JSON). See console for preview.'); } try{ const j = JSON.parse(text); if(!j.ok) throw new Error(j.msg || 'Server returned ok=false'); return j; }catch(err){ console.error('Failed to parse JSON response for', url, '->', text); throw new Error('Bad JSON response: ' + (err.message || 'parse error')); } } /* parse meters */ 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; } /* initial loaders */ 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); } const oldDiag = document.getElementById('khataDiag'); if(oldDiag) oldDiag.remove(); }catch(e){ console.error('khata_list load failed:', e); kh.innerHTML = '<option value="">(failed to load khatas)</option>'; let diag = document.getElementById('khataDiag'); if(!diag){ diag = document.createElement('div'); diag.id = 'khataDiag'; diag.style.marginTop = '6px'; diag.style.fontSize = '13px'; diag.style.color = '#b91c1c'; const parent = kh.parentElement || kh.closest('.row') || document.querySelector('.card'); if(parent) parent.insertBefore(diag, kh.nextSibling); } diag.innerHTML = `<strong>Failed to load khata list.</strong> <div style="margin-top:6px">Possible causes: session expired (login required) or server error. Open <code>?act=khata_list</code> in a new tab to see raw response.</div> <div style="margin-top:6px"><button id="khataRetryBtn" class="btn ghost" style="padding:6px 8px">Retry</button></div>`; const rb = document.getElementById('khataRetryBtn'); rb.addEventListener('click', ()=>{ diag.innerHTML=''; loadInit(); }); } 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); } window.__qualityList = ql.qualities || []; }catch(err){ console.warn('quality_list fetch failed:', err); } } 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 + auto-load summary */ $('machine').addEventListener('change', async (e)=>{ const m = parseInt(e.target.value||0); updateMachineNavDisplay(); 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(err){ console.warn('quality fetch failed', err); } finally{ try{ await new Promise(r=>setTimeout(r,50)); const kh = $('khata') ? $('khata').value.trim() : ''; const mo = $('month') ? $('month').value.trim() : ''; if (kh && mo) $('btnLoad').click(); }catch(e){} } }); function updateMachineNavDisplay(){ const box = $('machineDisplay'); const msel = document.getElementById('machine'); const val = parseInt(msel.value || 0); box.textContent = val? String(val) : '—'; } /* 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 */ $('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 (note: duplicated footer total included) */ 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(); const target = ev.target; const raw = (target.value||'').toString().trim(); if(raw===''){ const curRow = target.closest('.kg-row'); focusNextDateInput(curRow); return; } handleQuickAddEnter(target); }); right.appendChild(inp); } row.appendChild(ld); row.appendChild(right); datesWrap.appendChild(row); }); card.appendChild(datesWrap); const footer = document.createElement('div'); footer.className='kg-footer'; const ftLeft = document.createElement('div'); ftLeft.style.fontWeight='700'; ftLeft.textContent = ''; const ftRight = document.createElement('div'); const ftTotal = document.createElement('div'); ftTotal.className='kg-total'; ftTotal.textContent = '0 mtr'; ftRight.appendChild(ftTotal); footer.appendChild(ftLeft); footer.appendChild(ftRight); card.appendChild(footer); updateCardTotal(card); return card; } /* 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'; const list = (window.__flipColumns) ? data.karigars.slice().reverse() : data.karigars; list.forEach(k=>{ grid.appendChild(renderCardForKarigar(k)); }); wrap.appendChild(grid); document.querySelectorAll('.card-kg').forEach(c=>updateCardTotal(c)); window.__lastSummaryData.dates = data.dates || []; updateMachineNavDisplay(); } /* update totals */ 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 spans = card.querySelectorAll('.kg-total'); spans.forEach(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||'')) : ''; } /* quick add */ 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 (unchanged UI) */ 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 = []; 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); 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); 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'; inpt.addEventListener('keydown', ev=>{ if(ev.key==='Enter'){ ev.preventDefault(); saveBtn && saveBtn.click(); } if(ev.key==='Delete'){ ev.preventDefault(); deleteBtn && deleteBtn.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}); }); 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'; const deleteBtn=document.createElement('button'); deleteBtn.className='btn ghost'; deleteBtn.style.marginLeft='8px'; deleteBtn.textContent='Delete Entry'; deleteBtn.onclick=async()=>{ if(!confirm('Delete this entry permanently? This cannot be undone.')) return; const firstMeta=inputsMeta[0]; if(!firstMeta||!firstMeta.entry_id) return alert('No entry selected for delete'); try{ deleteBtn.disabled=true; deleteBtn.textContent='Deleting…'; const resp=await fetch('?act=delete_entry',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({entry_id:Number(firstMeta.entry_id)})}); const j=await resp.json(); if(!j.ok) throw new Error(j.msg||'Server error'); alert('Deleted entry #'+j.deleted_id); $('btnLoad').click(); }catch(err){ alert('Delete failed: '+(err.message||err)); deleteBtn.disabled=false; deleteBtn.textContent='Delete Entry'; } }; saveBtn.onclick=async()=>{ const map={}; const qualityForEntry={}; for(const meta of inputsMeta){ const raw=(meta.el.value||'').toString().trim(); if(raw===''||meta.entry_id<=0) continue; 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)}); } 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})}); await new Promise(r=>setTimeout(r,150)); $('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); actions.appendChild(deleteBtn); right.appendChild(actions); window.__currentDeleteEntryId=inputsMeta[0]?inputsMeta[0].entry_id:null; }catch(err){ alert('Edit load failed: '+err.message); } } 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; 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 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); });