« Back to History
yarn_entry.php
|
20260722_120325.php
Initial Domain Snapshot
Copy Code
<?php /* ========================================================= File: /erp/yarn_entry.php Purpose: Yarn/TFO/TPM/Zari/Kasab In-Out Entry Form (page-local) ========================================================= */ error_reporting(E_ALL); ini_set('display_errors',1); /* ---- Auth + scope ---- */ require_once __DIR__ . '/modules/auth/page_acl.php'; $ctx = page_require_access('yarn_entry'); $company_id = (int)($ctx['company_id'] ?? 3); $pdo = $ctx['pdo'] ?? null; $u = $ctx['user'] ?? null; /* ---- JSON helper ---- */ function jsend($ok,$data=[],$code=200){ while (ob_get_level()>0) { @ob_end_clean(); } if (!headers_sent()) { header_remove(); header('Content-Type: application/json; charset=utf-8'); http_response_code($code); } echo json_encode( $ok ? ['ok'=>true,'data'=>$data] : ['ok'=>false,'error'=>$data], JSON_UNESCAPED_UNICODE ); exit; } function normalize_box_qty($value){ $value = (float)$value; $rounded = round($value); return (abs($value - $rounded) <= 0.01) ? (float)$rounded : round($value, 3); } /* ---- AJAX guard ---- */ if (isset($_GET['ajax'])) { if (!is_array($ctx) || empty($ctx['user'] ?? null)) { jsend(false,'unauthenticated',401); } set_error_handler(function($severity,$message,$file,$line){ if (!(error_reporting() & $severity)) return false; throw new ErrorException($message,0,$severity,$file,$line); }); @ini_set('display_errors','0'); } /* ---- PDO bootstrap ---- */ if (!($pdo instanceof PDO)) { $tryInclude = function(string $path) use (&$pdo) { @include_once $path; if ($pdo instanceof PDO) return true; if (isset($db) && $db instanceof PDO) { $pdo = $db; return true; } if (isset($dbh) && $dbh instanceof PDO) { $pdo = $dbh; return true; } if (isset($conn) && $conn instanceof PDO){ $pdo = $conn; return true; } return false; }; foreach ([__DIR__.'/core/db.php', __DIR__.'/db.php', __DIR__.'/config/db.php', __DIR__.'/config.php'] as $p) { if (is_file($p) && $tryInclude($p)) break; } if (!($pdo instanceof PDO)) { http_response_code(500); exit('DB bootstrap failed.'); } } /* ---- legacy header expectation ---- */ if (!isset($company) || !is_array($company)) { $company = ['id'=>$company_id, 'name'=>($u['company_name'] ?? '')]; } /* ---- switches ---- */ $MATERIALS = ['yarn','tfo','tpm','zari','kasab']; $FLOWS = ['in','out']; $init_material = isset($_POST['material']) && in_array(strtolower($_POST['material']),$MATERIALS,true) ? strtolower($_POST['material']) : 'yarn'; $init_flow = isset($_POST['flow']) && in_array(strtolower($_POST['flow']),$FLOWS,true) ? strtolower($_POST['flow']) : 'in'; $init_ytype = trim($_POST['yarn_type'] ?? ''); $init_company = trim($_POST['company'] ?? ''); $init_denier = trim($_POST['denier'] ?? ''); $init_color = trim($_POST['color'] ?? ''); $init_assign = trim($_POST['assign_to'] ?? ''); /* ---- ensure IO tables ---- */ function ensure_io_table(PDO $pdo, string $name){ $sql = "CREATE TABLE IF NOT EXISTS `$name` ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, txn_date DATE NOT NULL, yarn_type VARCHAR(100) NOT NULL, company VARCHAR(150) NOT NULL, denier VARCHAR(50) NOT NULL, color VARCHAR(80) NOT NULL, boxes DECIMAL(12,3) NOT NULL DEFAULT 0, weight DECIMAL(14,3) NOT NULL DEFAULT 0, assign_to VARCHAR(150) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_company (company_id), INDEX idx_keys (yarn_type,company,denier,color), INDEX idx_date (txn_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"; $pdo->exec($sql); } foreach($MATERIALS as $m){ ensure_io_table($pdo,"{$m}_in"); ensure_io_table($pdo,"{$m}_out"); } /* ---- Assign master ensure ---- */ $pdo->exec("CREATE TABLE IF NOT EXISTS yarn_assigne ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, name VARCHAR(120) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uniq_company_name (company_id, name), INDEX idx_company (company_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); /* ---- Source table ensure ---- */ $pdo->exec("CREATE TABLE IF NOT EXISTS yarn_company_data ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, stock_type VARCHAR(50) NOT NULL, yarn_types VARCHAR(120) NULL, company_name VARCHAR(150) NULL, denier VARCHAR(50) NULL, color VARCHAR(80) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_company (company_id), INDEX idx_keys (company_id, stock_type, yarn_types, company_name, denier, color) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); /* ========================================================= AJAX HANDLER: options ========================================================= */ if (isset($_GET['ajax']) && $_GET['ajax']==='options') { try { $level = strtolower($_GET['level'] ?? 'type'); $material = strtolower($_GET['material'] ?? 'yarn'); $okL = ['type','company','denier','color']; if(!in_array($level,$okL,true)) jsend(false,'bad level',400); $mapMaterialToStockType = [ 'yarn'=>'Yarn','tfo'=>'TFO','tpm'=>'TPM','zari'=>'Zari','kasab'=>'Kasab' ]; $stockType = $mapMaterialToStockType[$material] ?? 'Yarn'; $yt = trim($_GET['yarn_type'] ?? ''); $co = trim($_GET['company'] ?? ''); $de = trim($_GET['denier'] ?? ''); $filters = [ 'company_id = :cid', 'TRIM(stock_type) COLLATE utf8mb4_unicode_ci = TRIM(:st) COLLATE utf8mb4_unicode_ci' ]; $params = [':cid'=>$company_id, ':st'=>$stockType]; if ($level !== 'type' && $yt !== '') { $filters[] = 'TRIM(yarn_types) COLLATE utf8mb4_unicode_ci = TRIM(:yt) COLLATE utf8mb4_unicode_ci'; $params[':yt'] = $yt; } if (in_array($level,['denier','color','company'],true) && $co !== '') { $filters[] = 'TRIM(company_name) COLLATE utf8mb4_unicode_ci = TRIM(:co) COLLATE utf8mb4_unicode_ci'; $params[':co'] = $co; } if ($level === 'color' && $de !== '') { $filters[] = 'TRIM(denier) COLLATE utf8mb4_unicode_ci = TRIM(:de) COLLATE utf8mb4_unicode_ci'; $params[':de'] = $de; } $colMap = [ 'type' => 'yarn_types', 'company' => 'company_name', 'denier' => 'denier', 'color' => 'color' ]; $col = $colMap[$level] ?? null; if(!$col) jsend(false,'bad col',400); $sql = "SELECT DISTINCT TRIM($col) COLLATE utf8mb4_unicode_ci AS v FROM yarn_company_data WHERE ".implode(' AND ',$filters)." AND $col IS NOT NULL AND TRIM($col) <> '' ORDER BY v"; $st = $pdo->prepare($sql); $st->execute($params); $out=[]; while($r=$st->fetch(PDO::FETCH_ASSOC)){ $v = trim((string)$r['v']); if($v!=='') $out[]=$v; } jsend(true,['options'=>$out]); } catch(Throwable $e) { jsend(false,'server: '.$e->getMessage(),500); } } /* =================== PART-2 (continuation) =================== Remaining AJAX handlers (assign_options, assign_add, stock, records), POST insert handler, HTML + JS for the page (form, dropdowns, segs). This continues immediately after PART-1. ============================================================ */ /* ---- AJAX: assign options ---- */ if (isset($_GET['ajax']) && $_GET['ajax'] === 'assign_options') { try { $st = $pdo->prepare("SELECT name FROM yarn_assigne WHERE company_id = ? ORDER BY name"); $st->execute([$company_id]); $opts = array_map(fn($r)=>$r['name'], $st->fetchAll(PDO::FETCH_ASSOC)); jsend(true, ['options'=>$opts]); } catch (Throwable $e) { jsend(false, 'server: '.$e->getMessage(), 500); } } /* ---- AJAX: assign add ---- */ if (isset($_GET['ajax']) && $_GET['ajax'] === 'assign_add') { try { $name = trim($_POST['name'] ?? ''); if ($name === '') jsend(false,'Name required',400); if (mb_strlen($name) > 120) jsend(false,'Name too long (max 120)',400); $ins = $pdo->prepare("INSERT IGNORE INTO yarn_assigne (company_id, name) VALUES (?, ?)"); $ok = $ins->execute([$company_id, $name]); if (!$ok) jsend(false,'DB insert failed',500); jsend(true, ['created'=>($ins->rowCount()>0), 'name'=>$name]); } catch (Throwable $e) { jsend(false, 'server: '.$e->getMessage(), 500); } } /* ---- AJAX: stock (current / future / last entry) ---- */ if (isset($_GET['ajax']) && $_GET['ajax'] === 'stock') { try { $material = strtolower($_GET['material'] ?? 'yarn'); $flow = strtolower($_GET['flow'] ?? 'in'); if (!in_array($material, $MATERIALS, true) || !in_array($flow, $FLOWS, true)) jsend(false,'bad material/flow',400); $yt = trim($_GET['yarn_type'] ?? ''); $co = trim($_GET['company'] ?? ''); $de = trim($_GET['denier'] ?? ''); $cl = trim($_GET['color'] ?? ''); $tbl = "{$material}_{$flow}"; $sqlL = "SELECT id, txn_date, yarn_type, company, denier, color, boxes, weight, assign_to, created_at FROM `$tbl` WHERE company_id = :cid ORDER BY id DESC LIMIT 1"; $st = $pdo->prepare($sqlL); $st->execute([':cid'=>$company_id]); $last = $st->fetch(PDO::FETCH_ASSOC); $haveKeys = ($yt !== '' && $co !== '' && $de !== '' && $cl !== ''); $current = null; $future = null; $form = ['boxes'=>0,'weight'=>0,'avg'=>0]; $diff_pct = null; $status = 'NA'; if ($haveKeys) { $filters = ['company_id=:cid','yarn_type=:yt','company=:co','denier=:de','color=:cl']; $params = [':cid'=>$company_id,':yt'=>$yt,':co'=>$co,':de'=>$de,':cl'=>$cl]; $sumSQL = fn($tbl,$filters)=>"SELECT COALESCE(SUM(boxes),0) boxes, COALESCE(SUM(weight),0) weight FROM `$tbl` WHERE ".implode(' AND ',$filters); $st = $pdo->prepare($sumSQL("{$material}_in",$filters)); $st->execute($params); $in = $st->fetch(PDO::FETCH_ASSOC) ?: ['boxes'=>0,'weight'=>0]; $st = $pdo->prepare($sumSQL("{$material}_out",$filters)); $st->execute($params); $out = $st->fetch(PDO::FETCH_ASSOC) ?: ['boxes'=>0,'weight'=>0]; $cur_boxes = normalize_box_qty(max(0.0,(float)$in['boxes'] - (float)$out['boxes'])); $cur_weight = max(0.0,(float)$in['weight'] - (float)$out['weight']); $cur_avg = ($cur_boxes>0) ? ($cur_weight/$cur_boxes) : 0.0; $f_boxes = normalize_box_qty(max(0.0,(float)($_GET['form_boxes'] ?? 0))); $f_weight = max(0.0,(float)($_GET['form_weight'] ?? 0)); if ($flow === 'in') { $fut_boxes = normalize_box_qty($cur_boxes + $f_boxes); $fut_weight = $cur_weight + $f_weight; } else { $fut_boxes = normalize_box_qty(max(0,$cur_boxes - $f_boxes)); $fut_weight = max(0,$cur_weight - $f_weight); } $fut_avg = ($fut_boxes>0) ? ($fut_weight/$fut_boxes) : 0.0; $form_avg = ($f_boxes>0) ? ($f_weight/$f_boxes) : 0.0; if ($cur_boxes == 0 && $flow === 'in') { $diff_pct = 0.0; $status = 'OK'; } else { $base = ($cur_avg>0) ? $cur_avg : ($form_avg>0 ? $form_avg : 1); $diff_pct = ($base>0) ? (($form_avg - $cur_avg)/$base*100.0) : 0.0; $abs = abs($diff_pct); $status = ($abs<=20)?'OK':(($abs<=30)?'NOT_OK':'PROBLEM'); } $current = ['boxes'=>round($cur_boxes,3),'weight'=>round($cur_weight,3),'avg_box_weight'=>round($cur_avg,3)]; $future = ['boxes'=>round($fut_boxes,3),'weight'=>round($fut_weight,3),'avg_box_weight'=>round($fut_avg,3)]; $form = ['boxes'=>$f_boxes,'weight'=>$f_weight,'avg'=>$form_avg]; } jsend(true,[ 'current'=>$current, 'future'=>$future, 'form'=>$form, 'diff_pct'=> is_null($diff_pct)? null : round($diff_pct,2), 'status'=>$status, 'target_table'=>$tbl, 'last_entry'=>$last ]); } catch (Throwable $e) { jsend(false,'server: '.$e->getMessage(),500); } } /* ---- AJAX: records ---- */ if (isset($_GET['ajax']) && $_GET['ajax'] === 'records') { try { $material = strtolower($_GET['material'] ?? 'yarn'); $flow = strtolower($_GET['flow'] ?? 'in'); if (!in_array($material, $MATERIALS, true) || !in_array($flow, $FLOWS, true)) jsend(false,'bad material/flow',400); $yt = trim($_GET['yarn_type'] ?? ''); $co = trim($_GET['company'] ?? ''); $de = trim($_GET['denier'] ?? ''); $cl = trim($_GET['color'] ?? ''); $tbl="{$material}_{$flow}"; $filters=['company_id=:cid']; $params=[ ':cid'=>$company_id ]; if($yt!==''){ $filters[]='yarn_type=:yt'; $params[':yt']=$yt; } if($co!==''){ $filters[]='company=:co'; $params[':co']=$co; } if($de!==''){ $filters[]='denier=:de'; $params[':de']=$de; } if($cl!==''){ $filters[]='color=:cl'; $params[':cl']=$cl; } $sql="SELECT id, txn_date, yarn_type, company, denier, color, boxes, weight, assign_to, created_at FROM `$tbl` WHERE ".implode(' AND ',$filters)." ORDER BY id DESC LIMIT 50"; $st=$pdo->prepare($sql); $st->execute($params); jsend(true,['rows'=>$st->fetchAll(PDO::FETCH_ASSOC),'table'=>$tbl]); } catch (Throwable $e) { jsend(false,'server: '.$e->getMessage(),500); } } /* ---------------- POST: Insert Handler ---------------- */ $flash = null; require_once __DIR__ . '/helpers/activity_helper.php'; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['do'] ?? '') === 'save') { $material = $init_material; $flow = $init_flow; $tbl = "{$material}_{$flow}"; $txn_date = trim($_POST['txn_date'] ?? ''); $ytype = trim($_POST['yarn_type'] ?? ''); $company = trim($_POST['company'] ?? ''); $denier = trim($_POST['denier'] ?? ''); $color = trim($_POST['color'] ?? ''); $boxes_input = (float)($_POST['boxes'] ?? 0); $boxes = normalize_box_qty($boxes_input); $weight = (float)($_POST['weight'] ?? 0); $assign_to = null; if (in_array($material, ['yarn','tpm'], true) && $flow === 'out') { $assign_to = trim($_POST['assign_to'] ?? '') ?: null; } $errs = []; if ($txn_date === '') $errs[] = 'Date required'; if ($ytype === '') $errs[] = 'Yarn Type required'; if ($company === '') $errs[] = 'Company required'; if ($denier === '') $errs[] = 'Denier required'; if ($color === '') $errs[] = 'Color required'; if ($boxes <= 0) $errs[] = 'Boxes must be > 0'; if (abs($boxes_input - round($boxes_input)) > 0.01) $errs[] = 'Boxes must be whole number'; if ($weight <= 0) $errs[] = 'Weight must be > 0'; if (in_array($material, ['yarn','tpm'], true) && $flow === 'out' && !$assign_to) $errs[] = 'Assign is required for Yarn / TPM Out'; if ($errs) { $flash = ['type'=>'error','msg'=>implode(', ',$errs)]; } else { $st = $pdo->prepare("INSERT INTO `$tbl` (company_id, txn_date, yarn_type, company, denier, color, boxes, weight, assign_to) VALUES (:cid,:dt,:yt,:co,:de,:cl,:bx,:wt,:asgn)"); $ok = $st->execute([ ':cid'=>$company_id, ':dt'=>$txn_date, ':yt'=>$ytype, ':co'=>$company, ':de'=>$denier, ':cl'=>$color, ':bx'=>$boxes, ':wt'=>$weight, ':asgn'=>$assign_to ]); $flash = $ok ? ['type'=>'success','msg'=>"Saved to $tbl"] : ['type'=>'error','msg'=>"DB error while saving."]; } // --- Activity Log --- if ($ok) { $mat = $material; $type = $ytype; $co = $company; $de = $denier; $cl = $color; $bx = is_numeric($boxes) ? (string)$boxes : number_format((float)$boxes, 3); $wt = is_numeric($weight) ? number_format((float)$weight, 2) : (string)$weight; $asgn = $assign_to ?? '-'; $remarks = sprintf( 'Material: %s, Type: %s, Company: %s, Denier: %s, Color: %s, Boxes: %s, Weight: %s, Assign: %s', $mat, $type, $co, $de, $cl, $bx, $wt, $asgn ); if (function_exists('activity_create')) { activity_create('yarn_entry', 'create', (int)$pdo->lastInsertId(), $remarks); } elseif (function_exists('log_activity')) { log_activity([ 'activity_type' => 'create', 'module_name' => 'yarn_entry', 'action_name' => 'create', 'activity_note' => $remarks, 'reference_id' => (int)$pdo->lastInsertId(), ]); } } } /* ---------- HEADER / FOOTER include decision (AJAX/export skip) ---------- */ $__header = __DIR__ . '/partials/header.php'; $__footer = __DIR__ . '/partials/footer.php'; if (!isset($USE_HEADER)) { $IS_AJAX = !empty($_GET['ajax']) || !empty($_GET['act']) || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); $IS_EXPORT = isset($_GET['fmt']) || isset($_GET['download']); $USE_HEADER = !($IS_AJAX || $IS_EXPORT); } if (!isset($PAGE_ID) || $PAGE_ID === '') { $PAGE_ID = 'yarn_entry'; } $PAGE = $PAGE_ID; ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Yarn Entry • Mister Manager</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="<?=htmlspecialchars($cssPath)?>"> </head> <body class="page-yarn-entry"> <?php if ($USE_HEADER && is_file($__header)) { include_once $__header; } ?> <div class="container-fluid py-4 px-4"> <div class="card border-0 shadow-sm mb-4"> <div class="card-body d-flex flex-wrap justify-content-between align-items-center gap-3"> <div class="btn-group shadow-sm" id="segMaterial"> <button class="btn btn-outline-primary fw-bold" data-v="yarn">Yarn</button> <button class="btn btn-outline-primary fw-bold" data-v="tfo">TFO</button> <button class="btn btn-outline-primary fw-bold" data-v="tpm">TPM</button> <button class="btn btn-outline-primary fw-bold" data-v="zari">Zari</button> <button class="btn btn-outline-primary fw-bold" data-v="kasab">Kasab</button> </div> <a class="btn btn-dark fw-bold shadow-sm px-4" href="/erp/yarn_stock.php" target="_blank"> <i class="bi bi-box-seam me-2"></i>Yarn Stock (Summary) </a> <div class="btn-group shadow-sm" id="segFlow"> <button class="btn btn-outline-success fw-bold px-4" data-v="in">In</button> <button class="btn btn-outline-danger fw-bold px-4" data-v="out">Out</button> </div> </div> </div> <?php if($flash): ?> <div class="alert alert-<?= ($flash['type'] === 'success') ? 'success' : 'danger' ?> alert-dismissible fade show border-0 shadow-sm mb-4"> <?= htmlspecialchars($flash['msg']) ?> <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> <?php endif; ?> <div class="row g-4"> <div class="col-md-3"> <div class="card border-0 shadow-sm h-100"> <div class="card-body text-center p-4"> <h6 class="text-uppercase fw-bold text-muted small mb-3">AVG Box Weight Difference</h6> <h4 class="fw-bold mb-2" id="diffLine">—</h4> <div id="diffPill" class="d-inline-block"></div> <hr class="my-4 text-secondary opacity-25"> <h6 class="text-uppercase fw-bold text-muted small mb-2">Last Entry</h6> <div class="fw-semibold text-dark p-2 bg-light rounded shadow-sm" id="lastEntry">—</div> </div> </div> </div> <div class="col-md-6"> <div class="card border-0 shadow-sm"> <div class="card-header bg-white py-3 border-0"> <h5 class="fw-bold mb-0 text-primary">Yarn Entry Form</h5> </div> <div class="card-body p-4 bg-light"> <form method="post" id="entryForm" autocomplete="off"> <input type="hidden" name="do" value="save"> <input type="hidden" name="material" id="f_material" value="<?= htmlspecialchars($init_material) ?>"> <input type="hidden" name="flow" id="f_flow" value="<?= htmlspecialchars($init_flow) ?>"> <div class="row mb-3"> <div class="col-md-6"> <label class="form-label small fw-bold">Date</label> <input type="date" name="txn_date" id="f_date" class="form-control border-2 shadow-sm" required value="<?= htmlspecialchars($_POST['txn_date'] ?? date('Y-m-d')) ?>"> </div> <div class="col-md-6"> <label class="form-label small fw-bold">Yarn Type</label> <select name="yarn_type" id="f_yarn_type" class="form-select border-2 shadow-sm" required></select> </div> </div> <div class="row mb-3"> <div class="col-md-6"> <label class="form-label small fw-bold">Company</label> <div class="input-group shadow-sm"> <select name="company" id="f_company" class="form-select border-2" required></select> <a href="/erp/yarn_company_add.php" target="_blank" class="btn btn-outline-secondary border-2" title="Add Company">+</a> </div> </div> <div class="col-md-6"> <label class="form-label small fw-bold">Denier</label> <select name="denier" id="f_denier" class="form-select border-2 shadow-sm" required></select> </div> </div> <div class="row mb-3"> <div class="col-md-6"> <label class="form-label small fw-bold">Color</label> <select name="color" id="f_color" class="form-select border-2 shadow-sm" required></select> </div> <div class="col-md-6"> <label class="form-label small fw-bold">Box Quantity</label> <input type="number" step="1" min="0" name="boxes" id="f_boxes" class="form-control border-2 shadow-sm" required placeholder="e.g. 10" value="<?= (isset($flash['type']) && $flash['type']==='success') ? '' : htmlspecialchars($_POST['boxes'] ?? '') ?>"> </div> </div> <div class="row mb-4"> <div class="col-md-6"> <label class="form-label small fw-bold">Total Weight (kg)</label> <input type="number" step="0.001" min="0" name="weight" id="f_weight" class="form-control border-2 shadow-sm" required placeholder="e.g. 250" value="<?= (isset($flash['type']) && $flash['type']==='success') ? '' : htmlspecialchars($_POST['weight'] ?? '') ?>"> </div> <div class="col-md-6" id="assignWrap" style="display:none"> <label class="form-label small fw-bold text-danger">Assign To (Out Only)</label> <div class="input-group shadow-sm"> <select name="assign_to" id="f_assign" class="form-select border-2"></select> <a href="/erp/yarn_assign_add.php" target="_blank" class="btn btn-outline-danger border-2">+</a> </div> </div> </div> <div class="d-flex flex-wrap gap-2 pt-2"> <button class="btn btn-primary px-4 fw-bold shadow-sm" type="submit">Submit Entry</button> <button class="btn btn-outline-secondary px-3" type="button" id="btnRecords">View Records</button> <button class="btn btn-link text-decoration-none text-muted" type="button" id="btnReset">Reset</button> </div> </form> </div> </div> </div> <div class="col-md-3"> <div class="card border-0 shadow-sm mb-4"> <div class="card-body p-4 text-center"> <h6 class="text-uppercase fw-bold text-muted small mb-2">Current Stock</h6> <div class="h3 fw-bold text-primary mb-1" id="curStock">—</div> <p class="small text-muted mb-0">AVG Box Weight</p> </div> </div> <div class="card border-0 shadow-sm"> <div class="card-body p-4 text-center"> <h6 class="text-uppercase fw-bold text-muted small mb-2">Future Stock</h6> <div class="h3 fw-bold text-success mb-0" id="futStock">—</div> </div> </div> </div> </div> <div class="card border-0 shadow-sm mt-4 overflow-hidden"> <div class="card-header bg-white py-3"> <h6 class="fw-bold mb-0" id="dynTitle">—</h6> </div> <div class="card-body p-0" id="dynArea"> <div class="p-4 text-center text-muted">Records will appear here...</div> </div> </div> </div> <script> const $ = s => document.querySelector(s); const $$= s => Array.from(document.querySelectorAll(s)); function formatBoxQty(val){ const n = Number(val || 0); if (!Number.isFinite(n)) return '0'; return Math.abs(n - Math.round(n)) <= 0.01 ? String(Math.round(n)) : n.toFixed(3); } let state = { material: <?=json_encode($init_material)?>, flow: <?=json_encode($init_flow)?>, yarn_type: <?=json_encode($init_ytype)?>, company: <?=json_encode($init_company)?>, denier: <?=json_encode($init_denier)?>, color: <?=json_encode($init_color)?>, assign_to: <?=json_encode($init_assign)?>, form_boxes: parseInt(document.querySelector('#f_boxes').value||'0', 10)||0, form_weight: parseFloat(document.querySelector('#f_weight').value||'0')||0 }; function setSegActive(containerSel,val){ $$(containerSel+' button').forEach(b=>b.classList.toggle('active',b.dataset.v===val)); } /* load options for dependent selects */ function loadOptions(level){ const p=new URLSearchParams({ajax:'options',level,material:state.material}); if(state.yarn_type) p.append('yarn_type',state.yarn_type); if(state.company) p.append('company',state.company); if(state.denier) p.append('denier',state.denier); const url = location.pathname+'?'+p.toString(); return fetch(url, {cache:'no-store', headers:{'X-Requested-With':'XMLHttpRequest'}}) .then(async (r)=>{ const ct = r.headers.get('content-type') || ''; if(!r.ok){ const body=await r.text().catch(()=>'(no body)'); console.error('OPTIONS '+level+' HTTP', r.status, body); throw new Error('http '+r.status); } if(!ct.includes('application/json')){ const body=await r.text().catch(()=>'(no body)'); console.error('OPTIONS '+level+' non-JSON:', body); throw new Error('non-json'); } return r.json(); }) .then(j=>{ if(!j || !j.ok){ console.error('OPTIONS '+level+' JSON error:', j); return; } const opts=j.data.options||[]; const fill=(id,val)=>{ const el=document.querySelector(id); if(!el) return; el.innerHTML='<option value="">— Select —</option>'; opts.forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; el.appendChild(o); }); if(opts.length===1){ el.value = opts[0]; el.dispatchEvent(new Event('change', { bubbles:true })); } else if(val) { el.value = val; } }; if(level==='type') fill('#f_yarn_type',state.yarn_type); if(level==='company')fill('#f_company',state.company); if(level==='denier') fill('#f_denier',state.denier); if(level==='color') fill('#f_color',state.color); }) .catch(e=>{ console.error('OPTIONS '+level+' failed:', e); }); } function resetBelow(level){ if(level==='type'){ state.company=''; $('#f_company').innerHTML=''; } if(level==='type'||level==='company'){ state.denier=''; $('#f_denier').innerHTML=''; } if(level!=='color'){ state.color=''; $('#f_color').innerHTML=''; } } /* assign options loader */ function loadAssignOptions(selectVal){ const sel = $('#f_assign'); if(!sel) return Promise.resolve(); sel.innerHTML = '<option value="">— Select —</option>'; return fetch(location.pathname+'?ajax=assign_options') .then(r=>r.json()) .then(j=>{ if(!j.ok) return; (j.data.options||[]).forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; sel.appendChild(o); }); const add=document.createElement('option'); add.value='__addnew__'; add.textContent='+ Add New…'; sel.appendChild(add); if(selectVal){ sel.value = selectVal; } else if(state.assign_to){ sel.value = state.assign_to; } }); } function hookAssignAdd(){ const sel = $('#f_assign'); if(!sel || sel.dataset.bound === '1') return; sel.dataset.bound = '1'; sel.addEventListener('change', ()=>{ if(sel.value === '__addnew__'){ const nameRaw = prompt('Enter new Assign name:'); if(!nameRaw){ sel.value=''; return; } const name = nameRaw.trim(); if(!name){ sel.value=''; return; } const form = new FormData(); form.append('name', name); fetch(location.pathname+'?ajax=assign_add', {method:'POST', body:form}) .then(r=>r.json()) .then(j=>{ if(!j.ok){ alert('Failed to add: '+(j.error||'Unknown error')); sel.value=''; return; } state.assign_to = j.data.name; loadAssignOptions(j.data.name); }); } else { state.assign_to = sel.value; } }); } /* stock/future/last loader */ let lastStockResp = null; function loadStockPanel(){ const p=new URLSearchParams({ ajax:'stock', material:state.material, flow:state.flow, yarn_type:state.yarn_type||'', company:state.company||'', denier:state.denier||'', color:state.color||'', form_boxes:state.form_boxes||0, form_weight:state.form_weight||0 }); return fetch(location.pathname+'?'+p.toString()) .then(r=>r.json()) .then(j=>{ if(!j.ok) return; lastStockResp = j.data; const c=j.data.current, f=j.data.future; if(c){ $('#curStock').innerHTML=`<div>Boxes: <b>${formatBoxQty(c.boxes)}</b></div><div>Weight: <b>${c.weight}</b> kg</div><div>AVG/Box: <b>${c.avg_box_weight}</b> kg</div>`; } else { $('#curStock').textContent='—'; } if(f){ $('#futStock').innerHTML=`<div>Boxes: <b>${formatBoxQty(f.boxes)}</b></div><div>Weight: <b>${f.weight}</b> kg</div><div>AVG/Box: <b>${f.avg_box_weight}</b> kg</div>`; } else { $('#futStock').textContent='—'; } if(c && typeof j.data.diff_pct === 'number'){ $('#diffLine').textContent=`Form AVG: ${Number(j.data.form.avg||0).toFixed(3)} • Current AVG: ${c.avg_box_weight} • Diff: ${j.data.diff_pct}%`; let pill=`<span class="pill ok">OK (≤20%)</span>`; if(j.data.status==='NOT_OK') pill=`<span class="pill warn">Not OK (21–30%)</span>`; if(j.data.status==='PROBLEM') pill=`<span class="pill bad">Problem (>30%)</span>`; $('#diffPill').innerHTML=pill; } else { $('#diffLine').textContent='—'; $('#diffPill').innerHTML=`<span class="pill na">N/A</span>`; } const last=j.data.last_entry; if(last){ $('#lastEntry').innerHTML=`<div><b>${j.data.target_table}</b> • #${last.id}</div> <div class="muted">${last.txn_date}</div> <div>${last.yarn_type} • ${last.company} • ${last.denier} • ${last.color}</div> <div>Boxes: <b>${formatBoxQty(last.boxes)}</b>, Weight: <b>${Number(last.weight).toFixed(3)} kg</b>${last.assign_to?' • Assign: '+last.assign_to:''}</div>`; } else { $('#lastEntry').textContent='No entries yet.'; } }); } /* records view */ function loadRecordsView(){ $('#dynTitle').textContent= `${state.material.toUpperCase()} ${state.flow.toUpperCase()} — Entry Records (last 50)`; const p=new URLSearchParams({ ajax:'records', material:state.material, flow:state.flow, yarn_type:state.yarn_type||'', company:state.company||'', denier:state.denier||'', color:state.color||'' }); fetch(location.pathname+'?'+p.toString()) .then(r=>r.json()) .then(j=>{ if(!j.ok) return; const rows=j.data.rows||[]; if(rows.length===0){ $('#dynArea').innerHTML= '<div class="p-4 text-center text-muted">No data found.</div>'; return; } const html = ` <div class="table-responsive"> <table class="table table-sm table-bordered table-striped table-hover align-middle mb-0"> <thead class="table-light text-uppercase small text-center"> <tr> <th>ID</th> <th>Date</th> <th>Type</th> <th>Company</th> <th>Denier</th> <th>Color</th> <th class="text-end">Boxes</th> <th class="text-end">Weight</th> <th>Assign</th> </tr> </thead> <tbody> ${rows.map(r=>` <tr> <td class="text-center">${r.id}</td> <td>${r.txn_date}</td> <td>${r.yarn_type}</td> <td>${r.company}</td> <td>${r.denier}</td> <td>${r.color}</td> <td class="text-end fw-semibold">${formatBoxQty(r.boxes)}</td> <td class="text-end fw-semibold">${Number(r.weight).toFixed(3)}</td> <td>${r.assign_to||''}</td> </tr> `).join('')} </tbody> </table> </div>`; $('#dynArea').innerHTML = html; }); } /* segmented controls */ function applySegStates(){ setSegActive('#segMaterial',state.material); setSegActive('#segFlow',state.flow); $('#btnRecords').textContent = `${state.material.toUpperCase()} Entry Records`; const showAssign = (state.flow === 'out' && (state.material === 'yarn' || state.material === 'tpm')); $('#assignWrap').style.display = showAssign ? '' : 'none'; if(showAssign){ loadAssignOptions(state.assign_to || '').then(()=>{ hookAssignAdd(); }); } } $$('#segMaterial button').forEach(b=>b.addEventListener('click',()=>{ state.material=b.dataset.v; $('#f_material').value=state.material; state.yarn_type=state.company=state.denier=state.color=''; applySegStates(); initDropdowns(); loadStockPanel(); })); $$('#segFlow button').forEach(b=>b.addEventListener('click',()=>{ state.flow=b.dataset.v; $('#f_flow').value=state.flow; applySegStates(); loadStockPanel(); })); /* dependent dropdown chain */ function initDropdowns(){ loadOptions('type').then(()=>{ if(state.yarn_type){ $('#f_yarn_type').value=state.yarn_type; } return loadOptions('company'); }).then(()=>{ if(state.company){ $('#f_company').value=state.company; } return loadOptions('denier'); }).then(()=>{ if(state.denier){ $('#f_denier').value=state.denier; } return loadOptions('color'); }).then(()=>{ if(state.color){ $('#f_color').value=state.color; } loadStockPanel(); }); } $('#f_yarn_type').addEventListener('change',e=>{ state.yarn_type=e.target.value; resetBelow('type'); loadOptions('company').then(()=>loadStockPanel()); }); $('#f_company').addEventListener('change', e => { state.company = e.target.value; resetBelow('company'); loadOptions('denier').then(loadStockPanel); }); $('#f_denier').addEventListener('change',e=>{ state.denier=e.target.value; resetBelow('denier'); loadOptions('color').then(loadStockPanel); }); $('#f_color').addEventListener('change',e=>{ state.color=e.target.value; loadStockPanel(); }); /* numbers sync */ function syncFormNumbers(){ state.form_boxes=parseInt($('#f_boxes').value||'0', 10)||0; state.form_weight=parseFloat($('#f_weight').value||'0')||0; loadStockPanel(); } $('#f_boxes').addEventListener('input',syncFormNumbers); $('#f_weight').addEventListener('input',syncFormNumbers); $('#f_boxes').addEventListener('change',syncFormNumbers); $('#f_weight').addEventListener('change',syncFormNumbers); /* buttons */ $('#btnRecords').addEventListener('click',()=>loadRecordsView()); $('#btnReset').addEventListener('click',()=>{ state.yarn_type=state.company=state.denier=state.color=''; $('#f_yarn_type').innerHTML=''; $('#f_company').innerHTML=''; $('#f_denier').innerHTML=''; $('#f_color').innerHTML=''; $('#f_boxes').value=''; $('#f_weight').value=''; state.form_boxes=0; state.form_weight=0; initDropdowns(); $('#f_yarn_type').focus(); }); /* submit guard */ $('#entryForm').addEventListener('submit', async (e)=>{ e.preventDefault(); await loadStockPanel(); const j = lastStockResp; if(!$('#f_yarn_type').value || !$('#f_company').value || !$('#f_denier').value || !$('#f_color').value){ alert('Select Type/Company/Denier/Color first.'); return; } const cur = (j && j.current) ? j.current : {boxes:0, weight:0}; const formBoxes = parseFloat($('#f_boxes').value||'0')||0; const formWeight = parseFloat($('#f_weight').value||'0')||0; if(state.flow==='out'){ if(formBoxes > cur.boxes || formWeight > cur.weight){ alert('Stock minus ho jaayega. Please reduce Boxes/Weight or check stock.'); return; } } const diff = (j && typeof j.diff_pct==='number') ? Math.abs(Number(j.diff_pct||0)) : 0; if (diff > 30) { const ok = confirm(`AVG Box me ${diff}% ka difference hai.\nKya aap fir bhi entry karana chahte hai?`); if (!ok) { return; } } e.target.submit(); }); /* init */ applySegStates(); initDropdowns(); if(state.material==='yarn' && state.flow==='out'){ loadAssignOptions(state.assign_to || '').then(()=>{ hookAssignAdd(); }); } /* success reset */ <?php if(isset($flash['type']) && $flash['type']==='success'): ?> (function(){ document.querySelector('#f_boxes').value=''; document.querySelector('#f_weight').value=''; state.form_boxes=0; state.form_weight=0; loadStockPanel(); document.querySelector('#f_boxes').focus(); })(); <?php endif; ?> /* overflow fix for some browsers */ document.querySelectorAll('.card, .row, .row > div').forEach(el => { if(el && el.style) el.style.minWidth = '0'; }); </script> <?php if (isset($USE_HEADER) && $USE_HEADER && is_file($__footer)) { include_once $__footer; } ?> </body> </html>