« Back to History
loom_production_entry.php
|
20260921_175631.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_once __DIR__ . '/helpers/activity_helper.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 qualities_name_column($pdo){ static $nameColumn = null; if($nameColumn !== null) return $nameColumn; try{ $st = $pdo->query("SHOW COLUMNS FROM qualities"); $cols = $st->fetchAll(PDO::FETCH_ASSOC) ?: []; $fields = array_column($cols, 'Field'); if(in_array('quality_name', $fields, true)){ $nameColumn = 'quality_name'; } elseif(in_array('name', $fields, true)){ $nameColumn = 'name'; } else { $nameColumn = ''; } }catch(Throwable $e){ $nameColumn = ''; } return $nameColumn; } function qualities_has_look_color($pdo){ static $hasLookColor = null; if($hasLookColor !== null) return $hasLookColor; try{ $st = $pdo->query("SHOW COLUMNS FROM qualities LIKE 'look_color'"); $hasLookColor = (bool)$st->fetch(PDO::FETCH_ASSOC); }catch(Throwable $e){ $hasLookColor = false; } return $hasLookColor; } function quality_look_color_map($pdo,$company_id){ static $cache = []; if(isset($cache[$company_id])) return $cache[$company_id]; $cache[$company_id] = []; $nameColumn = qualities_name_column($pdo); if($nameColumn === '' || !qualities_has_look_color($pdo)) return $cache[$company_id]; try{ $sql = "SELECT {$nameColumn} AS quality_name, look_color FROM qualities WHERE company_id=? AND COALESCE(TRIM(look_color),'')<>'' ORDER BY updated_at DESC, id DESC LIMIT 1000"; $st = $pdo->prepare($sql); $st->execute([$company_id]); foreach(($st->fetchAll(PDO::FETCH_ASSOC) ?: []) as $row){ $key = trim(mb_strtolower((string)($row['quality_name'] ?? ''))); if($key === '' || isset($cache[$company_id][$key])) continue; $cache[$company_id][$key] = trim((string)($row['look_color'] ?? '')); } }catch(Throwable $e){ $cache[$company_id] = []; } return $cache[$company_id]; } 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!==''){ $sql = "SELECT quality_id FROM quality_rates WHERE company_id=? AND TRIM(LOWER(quality_name))=TRIM(LOWER(?)) ORDER BY updated_at DESC,id DESC LIMIT 1"; $st=$pdo->prepare($sql); $st->execute([$company_id,$qname]); $qualityRow = $st->fetch(PDO::FETCH_ASSOC) ?: []; $qid = (int)($qualityRow['quality_id'] ?? 0); $lookColorMap = quality_look_color_map($pdo, $company_id); $lookColor = $lookColorMap[trim(mb_strtolower($qname))] ?? ''; return ['quality_name'=>$qname,'quality_id'=>$qid,'look_color'=>$lookColor]; } } return null; } function quality_list($pdo,$company_id){ $sql = "SELECT quality_id,quality_name FROM quality_rates WHERE company_id=? ORDER BY updated_at DESC, quality_name LIMIT 500"; $st=$pdo->prepare($sql); $st->execute([$company_id]); $rows = $st->fetchAll(PDO::FETCH_ASSOC) ?: []; $lookColorMap = quality_look_color_map($pdo, $company_id); foreach($rows as &$row){ $key = trim(mb_strtolower((string)($row['quality_name'] ?? ''))); $row['look_color'] = $lookColorMap[$key] ?? ''; } unset($row); return $rows; } /* ----- 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(); // >>> 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(); // Activity log log_activity([ 'activity_type' => 'create', 'module_name' => 'Loom Production', 'action_name' => 'create_line', 'reference_id' => $nid, 'activity_note' => 'Loom line created' ]); 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(); // >>> 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(); // Activity log log_activity([ 'activity_type' => 'create', 'module_name' => 'Loom Production', 'action_name' => 'add_extra', 'reference_id' => $entry_id, 'activity_note' => 'Extra meter appended' ]); 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(); // Activity log log_activity([ 'activity_type' => 'create', 'module_name' => 'Loom Production', 'action_name' => 'add_extra', 'reference_id' => $nid, 'activity_note' => 'Extra meter created' ]); 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 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; $orig = json_decode($row['lines_json'] ?? '[]', true); if(!is_array($orig)) $orig=[]; $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)]; } $sum = 0.0; foreach($new_lines as $nl) $sum += floatval($nl['meter'] ?? 0); $sum = round($sum,2); $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), $sum, $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'=>$sum]; } $pdo->commit(); // Activity log (log all updated entry_ids as CSV) $updated_ids = array_column($updated, 'entry_id'); log_activity([ 'activity_type' => 'update', 'module_name' => 'Loom Production', 'action_name' => 'update_entries', 'reference_id' => $updated_ids ? implode(',', $updated_ids) : null, 'activity_note' => 'Loom entries updated' ]); 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{ $st = $pdo->prepare("SELECT id FROM {$ENTRY_TABLE} WHERE company_id=? AND id=? LIMIT 1"); $st->execute([$COMPANY_ID,$eid]); if(!$st->fetchColumn()){ jexit(['ok'=>false,'msg'=>'Entry not found'],404); } $del = $pdo->prepare("DELETE FROM {$ENTRY_TABLE} WHERE id=? AND company_id=?"); $del->execute([$eid,$COMPANY_ID]); // Activity log log_activity([ 'activity_type' => 'delete', 'module_name' => 'Loom Production', 'action_name' => 'delete_entry', 'reference_id' => $eid, 'activity_note' => 'Loom entry deleted' ]); jexit(['ok'=>true,'deleted_id'=>$eid,'rows'=>$del->rowCount()]); }catch(Throwable $e){ 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(); // >>> 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); $meter_sum = 0.0; foreach($lines as $ln) { $meter_sum += floatval($ln['meter'] ?? 0); } $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(); // Activity log log_activity([ 'activity_type' => 'save', 'module_name' => 'Loom Production', 'action_name' => 'save_period', 'reference_id' => $new_id, 'activity_note' => 'Loom period saved' ]); 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> /* ================= Page Base ================= */ 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:12px; box-shadow:0 2px 6px rgba(15,23,42,0.03) } .row{display:flex;gap:12px;align-items:end;flex-wrap:wrap} /* ================= Inputs ================= */ .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} /* ================= Buttons ================= */ .btn{ background:#2563eb; color:#fff; border:none; padding:8px 12px; border-radius:8px; cursor:pointer; box-shadow:0 2px 6px rgba(37,99,235,0.15) } .btn:hover{background:#1d4ed8} /* DULL EDIT BUTTON */ .btn.edit, .kg-row .btn{ background:#e5e7eb; color:#374151; box-shadow:none; } .kg-row .btn:hover{ background:#d1d5db; } /* Ghost buttons */ .btn.ghost{ background:#fff; color:#1e3a8a; border:1px solid #c7d2fe; } .btn.ghost:hover{ background:#eef2ff; } /* ================= Machine Nav ================= */ .navMachine{ display:flex; align-items:center; gap:8px; padding:6px 10px; border-radius:10px; border:1px solid #dbeafe; background:#f8fafc; } .navMachine button{ width:34px; height:34px; font-size:20px; font-weight:700; display:flex; align-items:center; justify-content:center; border-radius:8px; cursor:pointer; color:#1e3a8a; background:#e0e7ff; border:1px solid #c7d2fe; } .navMachine button:hover{background:#c7d2fe} .navMachine .machineBox{ min-width:64px; height:34px; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:15px; color:#0f172a; background:#ffffff; border:1px solid #c7d2fe; border-radius:8px; } /* ================= Karigar Grid – 5 per row ================= */ /* use CSS grid instead of flex */ .grid{ display:grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap:12px; margin-top:12px; width:100%; } /* card should flex inside grid cell */ .card-kg{ min-width:0; /* IMPORTANT: allow shrink */ max-width:none; width:100%; } /* Responsive safety */ @media (max-width:1600px){ .grid{ grid-template-columns: repeat(4, 1fr); } } @media (max-width:1300px){ .grid{ grid-template-columns: repeat(3, 1fr); } } @media (max-width:900px){ .grid{ grid-template-columns: repeat(2, 1fr); } } @media (max-width:520px){ .grid{ grid-template-columns: repeat(1, 1fr); } } /* ---- HEADER ---- */ .kg-head{ display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; } /* Karigar name highlight */ .kg-name{ font-weight:900; font-size:15px; color:#1e3a8a; background:#eef2ff; padding:4px 10px; border-radius:8px; } /* TOP total meter highlight */ .kg-total{ font-size:13px; font-weight:800; color:#065f46; background:#ecfdf5; padding:5px 10px; border-radius:10px; border:1px solid #bbf7d0; } /* Add button highlight */ .kg-head .btn.ghost{ font-weight:700; border-color:#93c5fd; color:#1e40af; } /* ================= Dates & Rows ================= */ .kg-dates{margin-top:6px} .kg-row{ display:flex; justify-content:space-between; align-items:center; padding:6px 0; border-top:1px dashed #e5e7eb; } .kg-row:first-child{border-top:none} /* DATE highlight */ .kg-row .date{ min-width:110px; font-size:13px; font-weight:700; color:#334155; background:#f1f5f9; padding:4px 8px; border-radius:8px; } /* Meter display */ .kg-row .taka{ background:#e0f2fe; color:#075985; padding:4px 10px; border-radius:10px; font-weight:800; } .kg-row.meter-alert-orange .taka, .kg-row.meter-alert-orange .date{ background:#ffedd5; color:#9a3412; } .kg-row.meter-alert-red .taka, .kg-row.meter-alert-red .date{ background:#fee2e2; color:#b91c1c; } /* Input */ .inlineMeter{ width:148px; padding:6px 8px; border:1px solid #cbd5e1; border-radius:6px; } .inlineMeter:focus{ outline:2px solid rgba(37,99,235,0.3) } .smallActions{display:flex;gap:8px;align-items:center} /* ================= FOOTER TOTAL ================= */ .kg-footer{ margin-top:10px; border-top:2px solid #e0e7ff; padding-top:8px; display:flex; justify-content:space-between; align-items:center; } /* Bottom total STRONG highlight */ .kg-footer .kg-total{ font-size:14px; font-weight:900; color:#7c2d12; background:#fff7ed; border:1px solid #fed7aa; padding:6px 14px; border-radius:12px; } /* ================= Misc ================= */ .placeholder{color:#6b7280;padding:12px} .totalsBar{ margin-top:12px; padding:10px; background:#fff; border:1px solid #e6e9ee; border-radius:10px; display:flex; justify-content:space-between; align-items:center } /* Flip */ .grid.flipColumns{flex-direction:row-reverse} /* Shortcuts */ .shortcutsBox{ margin-top:18px; padding:14px; border-radius:10px; background:#f8fafc; border:1px solid #e0e7ff; color:#374151 } kbd{ background:#f3f4f6; border-radius:4px; padding:3px 6px; border:1px solid #e6e7eb; font-weight:700; margin-right:6px } @media (max-width:920px){ .grid{flex-direction:column} .card-kg{max-width:100%} } /* ================= TOP TOOLBAR ================= */ .entry-shell{ padding-top:2px; } .entry-card-body{ padding-top:10px; } .entry-topbar{ position:sticky; top:0; z-index:20; display:flex; align-items:flex-end; gap:8px; flex-wrap:nowrap; overflow-x:auto; padding:4px 0 10px; margin-bottom:10px; background:#fff; } .entry-topbar__field, .entry-topbar__actions, .entry-topbar__nav, .entry-topbar__karigar, .entry-topbar__extra, .entry-topbar__extra-btn{ flex:0 0 auto; } .entry-topbar__field{min-width:140px} .entry-topbar__field--machine{min-width:88px} .entry-topbar__field--khata{min-width:180px} .entry-topbar__field--quality{min-width:120px} .entry-topbar__field--month{min-width:140px} .entry-topbar__field--period{min-width:90px} .entry-topbar__actions, .entry-topbar__nav{ display:flex; align-items:center; gap:8px; } .entry-topbar__karigar{min-width:170px} .entry-topbar__extra{min-width:130px} .entry-topbar__extra--meter{min-width:72px} .entry-topbar__extra-btn{min-width:64px} .entry-topbar .form-label{ margin-bottom:4px; font-size:12px; } .entry-topbar .btn, .entry-topbar #machineDisplay{ white-space:nowrap; } .entry-topbar__extra--meter input{ width:92px; min-width:92px; padding-left:8px; padding-right:8px; } .entry-topbar .btn{ box-shadow:none; } .entry-topbar .btn-primary{ background-color:#0d6efd; border-color:#0d6efd; color:#fff; } .entry-topbar .btn-primary:hover{ background-color:#0b5ed7; border-color:#0a58ca; } .entry-topbar .btn-outline-secondary{ background:#fff; color:#6c757d; border:1px solid #6c757d; } .entry-topbar .btn-outline-secondary:hover{ background:#6c757d; color:#fff; } .entry-topbar .btn-outline-dark{ background:#fff; color:#212529; border:1px solid #212529; } .entry-topbar .btn-outline-dark:hover{ background:#212529; color:#fff; } .entry-topbar .btn-outline-info{ background:#fff; color:#0dcaf0; border:1px solid #0dcaf0; } .entry-topbar .btn-outline-info:hover{ background:#0dcaf0; color:#000; } .entry-topbar .btn-outline-primary{ background:#fff; color:#0d6efd; border:1px solid #0d6efd; } .entry-topbar .btn-outline-primary:hover{ background:#0d6efd; color:#fff; } .quality-look-active{ transition:background-color .2s ease,border-color .2s ease,color .2s ease,box-shadow .2s ease; font-weight:700; } .kg-row-quality{ transition:background-color .2s ease,color .2s ease,border-color .2s ease,box-shadow .2s ease; } @media (max-width:1200px){ .entry-topbar{ position:static; flex-wrap:wrap; overflow-x:visible; } } </style> </head><body> <div class="container-fluid py-2 entry-shell"> <div class="card shadow-sm border-0"> <div class="card-body entry-card-body"> <div class="entry-topbar"> <div class="entry-topbar__field entry-topbar__field--khata"> <label class="form-label">Khata</label> <select id="khata" class="form-select"> <option>Loading…</option> </select> </div> <div class="entry-topbar__field entry-topbar__field--machine"> <label class="form-label">Machine</label> <select id="machine" class="form-select"> <option value="">—</option> </select> </div> <div class="entry-topbar__field entry-topbar__field--quality"> <label class="form-label">Quality</label> <select id="quality_select" class="form-select"> <option value="0">(select)</option> </select> </div> <div class="entry-topbar__field entry-topbar__field--month"> <label class="form-label">Month</label> <input id="month" type="month" class="form-control"> </div> <div class="entry-topbar__field entry-topbar__field--period"> <label class="form-label">Period</label> <select id="period" class="form-select"> <option value="H1">H1</option> <option value="H2">H2</option> </select> </div> <div class="entry-topbar__actions"> <button id="btnLoad" class="btn btn-primary"> Load → </button> <button id="btnClear" class="btn btn-outline-secondary"> Clear </button> </div> <div class="entry-topbar__nav"> <button id="btnPrevMachine" class="btn btn-outline-dark btn-sm">‹</button> <div id="machineDisplay" class="border rounded px-3 py-1 fw-bold bg-light text-center" style="min-width:55px;"> — </div> <button id="btnNextMachine" class="btn btn-outline-dark btn-sm">›</button> </div> <!-- Flip --> <button id="btnFlip" class="btn btn-outline-info btn-sm"> Flip order </button> <div class="entry-topbar__karigar"> <label class="form-label small mb-1">Add Karigar</label> <div class="input-group input-group-sm"> <select id="extraKarigar" class="form-select"> <option value="">Select</option> </select> <button id="btnAddKarigar" class="btn btn-outline-secondary"> Add </button> </div> </div> <div class="entry-topbar__extra"> <label class="form-label">Extra Date</label> <input id="extra_date" type="date" class="form-control"> </div> <div class="entry-topbar__extra entry-topbar__extra--meter"> <label class="form-label">Extra Meter</label> <input id="extra_meter" class="form-control" placeholder="e.g. 12 or 12+25"> </div> <div class="entry-topbar__extra-btn"> <label class="form-label"> </label> <button id="btnAddExtra" class="btn btn-outline-primary w-100"> Add </button> </div> </div> <!-- ================= SUMMARY ================= --> <div id="summaryWrap" class="mt-2"></div> <div id="grandTotals" class="alert alert-light border mt-3 d-none"> <div class="d-flex justify-content-between"> <div> <strong>Grand total:</strong> <span id="grandTotalValue">0</span> </div> <div id="qualityLegend"></div> </div> </div> <!-- ================= SHORTCUTS ================= --> <div class="card mt-4 border-0 bg-light"> <div class="card-body py-3"> <strong>Keyboard Shortcuts</strong> <div class="row mt-2 small"> <div class="col-md-6"> Ctrl + Enter → Machine </div> <div class="col-md-6"> Alt + Enter → Load </div> <div class="col-md-6"> Shift + Enter → Karigar select </div> <div class="col-md-6"> Ctrl + Shift + Enter → Add karigar </div> <div class="col-md-6"> Delete → Delete selected entry </div> <div class="col-md-6"> Enter on blank → Accept & move next </div> </div> </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; } function sumMeterText(raw){ if(typeof raw !== 'string') raw = String(raw || ''); return raw.split('+').reduce((sum, part)=>{ const value = parseFloat(String(part || '').trim()); return isNaN(value) ? sum : (sum + value); }, 0); } function applyDateThresholdHighlights(){ const rowsByDate = new Map(); document.querySelectorAll('.card-kg .kg-row').forEach(row=>{ row.classList.remove('meter-alert-orange', 'meter-alert-red'); const taka = row.querySelector('.taka'); const total = taka ? Math.round(sumMeterText(taka.textContent) * 100) / 100 : 0; if(total > 0 && total < 10){ row.classList.add('meter-alert-red'); } if(!row.dataset.date) return; if(!rowsByDate.has(row.dataset.date)) rowsByDate.set(row.dataset.date, []); rowsByDate.get(row.dataset.date).push({ row, total }); }); rowsByDate.forEach(items=>{ const filled = items.filter(item => item.total > 0); if(filled.length !== 2) return; const combinedTotal = filled.reduce((sum, item) => sum + item.total, 0); if(combinedTotal >= 120){ filled.forEach(item => item.row.classList.add('meter-alert-red')); return; } if(combinedTotal >= 110){ filled.forEach(item => item.row.classList.add('meter-alert-orange')); } }); } function normalizeLookColor(raw){ const value = String(raw || '').trim(); if(!value) return ''; const named = { red:'#ef4444', green:'#22c55e', blue:'#3b82f6', yellow:'#eab308', orange:'#f97316', purple:'#8b5cf6', pink:'#ec4899', black:'#111827', white:'#ffffff', grey:'#6b7280', gray:'#6b7280', brown:'#92400e' }; return named[value.toLowerCase()] || value; } function getContrastColor(hexOrName){ const color = normalizeLookColor(hexOrName); if(!color) return '#0f172a'; if(!color.startsWith('#')){ return ['yellow', 'white'].includes(String(hexOrName || '').trim().toLowerCase()) ? '#111827' : '#ffffff'; } let hex = color.slice(1); if(hex.length === 3){ hex = hex.split('').map(ch => ch + ch).join(''); } if(hex.length !== 6) return '#ffffff'; const r = parseInt(hex.slice(0,2), 16); const g = parseInt(hex.slice(2,4), 16); const b = parseInt(hex.slice(4,6), 16); const brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000; return brightness > 160 ? '#111827' : '#ffffff'; } function applyQualityLook(selectEl){ const qs = selectEl || $('quality_select'); if(!qs) return; const selected = qs.selectedOptions && qs.selectedOptions[0] ? qs.selectedOptions[0] : null; const rawColor = selected ? (selected.dataset.lookColor || '') : ''; const lookColor = normalizeLookColor(rawColor); const textColor = getContrastColor(lookColor); qs.classList.toggle('quality-look-active', !!lookColor); qs.style.backgroundColor = lookColor || '#ffffff'; qs.style.borderColor = lookColor || '#e2e8f0'; qs.style.color = lookColor ? textColor : '#111827'; qs.style.boxShadow = lookColor ? `0 0 0 1px ${lookColor}33` : 'none'; const legend = $('qualityLegend'); if(legend){ if(qs.value > 0 && selected){ legend.textContent = `Quality: ${selected.textContent}${rawColor ? ` | Look: ${rawColor}` : ''}`; legend.style.background = lookColor || 'transparent'; legend.style.color = lookColor ? textColor : '#111827'; legend.style.border = lookColor ? `1px solid ${lookColor}` : 'none'; legend.style.padding = lookColor ? '4px 10px' : '0'; legend.style.borderRadius = lookColor ? '999px' : '0'; legend.style.fontWeight = lookColor ? '700' : '400'; } else { legend.textContent = ''; legend.style.background = 'transparent'; legend.style.color = ''; legend.style.border = 'none'; legend.style.padding = '0'; legend.style.borderRadius = '0'; legend.style.fontWeight = '400'; } } } function getQualityMeta(qualityId){ const list = window.__qualityList || []; const target = Number(qualityId || 0); if(!target) return null; return list.find(q => Number(q.quality_id) === target) || null; } function pickPrimaryQuality(qualities){ const entries = Object.entries(qualities || {}); if(!entries.length) return null; entries.sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0)); return Number(entries[0][0] || 0) || null; } function applyRowQualityLook(row, qualityId){ if(!row) return; const takaBox = row.querySelector('.taka'); const fallbackQualityId = Number($('quality_select')?.value || 0); const resolvedQualityId = Number(qualityId || 0) || fallbackQualityId; const quality = getQualityMeta(resolvedQualityId); const rawColor = quality ? (quality.look_color || '') : ''; const lookColor = normalizeLookColor(rawColor); const textColor = getContrastColor(lookColor); row.dataset.qualityId = resolvedQualityId ? String(resolvedQualityId) : ''; if(takaBox){ takaBox.classList.toggle('kg-row-quality', !!lookColor); takaBox.style.backgroundColor = lookColor || ''; takaBox.style.color = lookColor ? textColor : ''; takaBox.style.border = lookColor ? `1px solid ${lookColor}` : ''; takaBox.style.boxShadow = lookColor ? `inset 0 0 0 1px ${lookColor}33` : ''; } if(takaBox && quality && quality.quality_name){ takaBox.title = `Quality: ${quality.quality_name}${rawColor ? ` | Color: ${rawColor}` : ''}`; } } function applyAllRenderedQualityLooks(){ document.querySelectorAll('.card-kg .kg-row').forEach(row => { const qualityId = Number(row.dataset.qualityId || 0); if(!row.querySelector('.taka')) return; applyRowQualityLook(row, qualityId); }); } /* 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; o.dataset.lookColor = q.look_color || ''; qs.appendChild(o); } window.__qualityList = ql.qualities || []; applyQualityLook(qs); applyAllRenderedQualityLooks(); }catch(err){ console.warn('quality_list fetch failed:', err); } } loadInit(); $('quality_select').addEventListener('change', ()=>{ applyQualityLook($('quality_select')); applyAllRenderedQualityLooks(); }); /* 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; opt.dataset.lookColor = q.quality.look_color || ''; qs.insertBefore(opt, qs.firstChild.nextSibling); } if(!opt.dataset.lookColor && q.quality.look_color){ opt.dataset.lookColor = q.quality.look_color; } qs.value = sid; applyQualityLook(qs); } } 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) { const b = $('btnLoad'); if (b) b.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 primaryQualityId = pickPrimaryQuality(d.qualities || {}) || Number($('quality_select')?.value || 0); 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); applyRowQualityLook(row, primaryQualityId); } else { const inp = document.createElement('input'); inp.type='text'; inp.className='inlineMeter'; 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 currentRow = target.closest('.kg-row'); focusNextDateInput(currentRow); return; } handleQuickAddEnter(target); }); right.appendChild(inp); } row.appendChild(ld); row.appendChild(right); datesWrap.appendChild(row); }); card.appendChild(datesWrap); // footer (duplicate total, shown at bottom) 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 || []; applyAllRenderedQualityLooks(); updateMachineNavDisplay(); } /* update totals: updates both top and footer 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=>{ sum += sumMeterText(s); }); 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(); applyDateThresholdHighlights(); } 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'; applyQualityLook($('quality_select')); } /* 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); applyRowQualityLook(row, qid); 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 and update_entries logic unchanged (keeps same UX) */ 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; o.dataset.lookColor = q.look_color || ''; if(entry.quality_id && Number(q.quality_id)===Number(entry.quality_id)) o.selected=true; qSel.appendChild(o); }); qSel.addEventListener('change', ()=>applyQualityLook(qSel)); applyQualityLook(qSel); 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 = ''; 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{ let focused = document.activeElement; let focusCard = focused ? focused.closest && focused.closest('.card-kg') : null; let focusRow = focused ? focused.closest && focused.closest('.kg-row') : null; if(focusCard && focusRow){ window.__focusAfter = { kid: focusCard.dataset.kg, date: focusRow.dataset.date }; } else { const first = inputsMeta[0]; if(first){ window.__focusAfter = { kid: String(first.karigar_id), date: first.date ? first.date.substr(0,10) : null }; } else { window.__focusAfter = null; } } const resp = await fetchJSON('?act=update_entries', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({edits}) }); await sleep(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); }); window.__qualityList = window.__qualityList || (await (async()=>{ const q=await fetchJSON('?act=quality_list'); return q.qualities||[]; })()); updateGrandTotal(); if(window.__focusAfter && window.__focusAfter.kid){ try{ const targetCard = document.querySelector(`.card-kg[data-kg="${window.__focusAfter.kid}"]`); if(targetCard){ const rows = Array.from(targetCard.querySelectorAll('.kg-row')); let idx = rows.findIndex(r => r.dataset.date === window.__focusAfter.date); if(idx === -1) idx = rows.findIndex(r => r.querySelector('input.inlineMeter')); if(idx >= 0){ for(let i = idx; i < rows.length; i++){ const inp = rows[i].querySelector('input.inlineMeter:not([disabled])'); if(inp && inp.value.trim() === ''){ inp.focus(); inp.select && inp.select(); break; } } if(document.activeElement.tagName !== 'INPUT'){ for(let i = idx-1; i >= 0; i--){ const inp = rows[i].querySelector('input.inlineMeter:not([disabled])'); if(inp && inp.value.trim() === ''){ inp.focus(); inp.select && inp.select(); break; } } } } } }catch(e){} window.__focusAfter = null; } }catch(err){ wrap.innerHTML = `<div class="placeholder" style="color:#b91c1c">Error: ${err.message}</div>`; } finally{ b.disabled=false; b.textContent='Load →'; updateMachineNavDisplay(); } }); /* Clear */ $('btnClear').addEventListener('click', ()=>{ $('khata').value=''; $('machine').innerHTML='<option value="">—</option>'; $('month').value=''; $('summaryWrap').innerHTML=''; $('grandTotals').style.display='none'; $('quality_select').value='0'; applyQualityLook($('quality_select')); $('khata').focus(); updateMachineNavDisplay(); }); /* Prev/Next */ $('btnPrevMachine').addEventListener('click', ()=>{ const ms = $('machine'); const cur = parseInt(ms.value||0); if(!cur) return; const prev = cur - 1; if([...ms.options].some(o=>parseInt(o.value||0)===prev)){ ms.value = String(prev); ms.dispatchEvent(new Event('change', { bubbles: true })); updateMachineNavDisplay(); } else { const lesser = [...ms.options].map(o=>parseInt(o.value||0)).filter(n=>n>0 && n<cur).sort((a,b)=>b-a)[0]; if(lesser){ ms.value = String(lesser); ms.dispatchEvent(new Event('change', { bubbles: true })); updateMachineNavDisplay(); } } }); $('btnNextMachine').addEventListener('click', ()=>{ const ms = $('machine'); const cur = parseInt(ms.value||0); if(!cur) return; const nxt = cur + 1; if([...ms.options].some(o=>parseInt(o.value||0)===nxt)){ ms.value = String(nxt); ms.dispatchEvent(new Event('change', { bubbles: true })); updateMachineNavDisplay(); } else { const greater = [...ms.options].map(o=>parseInt(o.value||0)).filter(n=>n>0 && n>cur).sort((a,b)=>a-b)[0]; if(greater){ ms.value = String(greater); ms.dispatchEvent(new Event('change', { bubbles: true })); updateMachineNavDisplay(); } } }); /* Flip order button */ $('btnFlip').addEventListener('click', ()=>{ window.__flipColumns = !window.__flipColumns; const grid = document.querySelector('.grid'); if(grid) grid.classList.toggle('flipColumns', !!window.__flipColumns); if(window.__lastSummaryData) renderKarigars(window.__lastSummaryData); }); /* ================= Keyboard Shortcuts ================= */ document.addEventListener('keydown', function(e){ /* ---------- Existing shortcuts ---------- */ if(e.key === "Enter" && e.ctrlKey && !e.shiftKey && !e.altKey){ e.preventDefault(); const m = document.getElementById('machine'); if(m){ m.focus(); try{ m.select(); }catch(_){} } return; } if(e.key === "Enter" && e.altKey && !e.ctrlKey && !e.shiftKey){ e.preventDefault(); const b = document.getElementById('btnLoad'); if(b) b.click(); return; } if(e.key === "Enter" && e.shiftKey && !e.ctrlKey && !e.altKey){ e.preventDefault(); const ek = document.getElementById('extraKarigar'); if(ek){ ek.focus(); } return; } if(e.key === "Enter" && e.ctrlKey && e.shiftKey){ e.preventDefault(); const ab = document.getElementById('btnAddKarigar'); if(ab) ab.click(); return; } if(e.key === "Delete" && !e.ctrlKey && !e.altKey){ const active = document.activeElement; if(active && (active.classList && (active.classList.contains('inlineMeter') || active.closest('.card-kg')))){ if(window.__currentDeleteEntryId){ if(confirm('Delete current entry #' + window.__currentDeleteEntryId + '?')){ fetch('?act=delete_entry', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ entry_id: Number(window.__currentDeleteEntryId) }) }) .then(r=>r.json()) .then(j=>{ if(!j.ok) return alert('Delete failed: '+(j.msg||'error')); alert('Deleted entry #' + j.deleted_id); $('btnLoad').click(); }) .catch(err=>alert('Delete failed: '+err)); } } } return; } /* ---------- NEW: Ctrl + 1 / 2 / 3 → Karigar quick select ---------- */ if (!e.ctrlKey || e.altKey || e.shiftKey) return; const map = { '1': 0, '2': 1, '3': 2, 'Numpad1': 0, 'Numpad2': 1, 'Numpad3': 2 }; if (!(e.key in map)) return; e.preventDefault(); const index = map[e.key]; const cards = document.querySelectorAll('.card-kg'); if (!cards[index]) return; const firstRow = cards[index].querySelector('.kg-row'); if (!firstRow) return; const input = firstRow.querySelector('input.inlineMeter:not([disabled])'); if (input) { input.focus(); input.select && input.select(); } }); window.addEventListener('load', ()=>{ updateMachineNavDisplay(); }); /* If header.php didn't obey $PAGE_HIDE_TOPBAR, run small fallback to hide common topbar selectors. This is intentionally page-local and minimal; replace with server-side header change if you prefer. */ (function(){ try{ if (typeof window !== 'undefined') { // run after small delay so header has rendered setTimeout(()=>{ // common header/topbar selectors used across installations - tweak if your header uses different IDs const selectors = ['#topbar','.topbar','#erp-topbar','.main-nav','.navbar','.header-menu']; selectors.forEach(s=>{ const el = document.querySelector(s); if(el) el.style.display='none'; }); },80); } }catch(e){} })(); </script> </body></html> <?php /* robust footer include with fallbacks */ $__footer_paths = [ __DIR__ . '/partials/footer.php', __DIR__ . '/erp/partials/footer.php', __DIR__ . '/../erp/partials/footer.php', __DIR__ . '/../../erp/partials/footer.php', $_SERVER['DOCUMENT_ROOT'] . '/erp/partials/footer.php', ]; $__footer_included = false; foreach ($__footer_paths as $__fp) { if ($__fp && file_exists($__fp)) { require_once $__fp; $__footer_included = true; break; } } if (! $__footer_included) { error_log('loom_production_entry.php: footer.php not found. Tried: ' . implode(' | ', $__footer_paths)); echo "<!-- WARNING: footer.php not found. checked: " . htmlentities(implode(', ', $__footer_paths)) . " -->\n"; } ?>