canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } $pdo = DB::pdo(); $q = trim((string)($_GET['q'] ?? '')); if ($q !== '') { $stmt = $pdo->prepare(" SELECT p.*, lob.code AS lob_code, lob.name AS lob_name FROM products p JOIN line_of_business lob ON lob.id = p.lob_id WHERE p.code LIKE :q OR lob.name LIKE :q OR lob.code LIKE :q ORDER BY p.id DESC "); $stmt->execute(['q' => "%$q%"]); } else { $stmt = $pdo->query(" SELECT p.*, lob.code AS lob_code, lob.name AS lob_name FROM products p JOIN line_of_business lob ON lob.id = p.lob_id ORDER BY p.id DESC "); } $products = $stmt->fetchAll(); require __DIR__ . '/../views/layout/header.php'; require __DIR__ . '/../views/products/index.php'; require __DIR__ . '/../views/layout/footer.php'; } public function create(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } $pdo = DB::pdo(); $product = null; $lobs = $pdo->query("SELECT id, code, name FROM line_of_business ORDER BY name ASC")->fetchAll(); require __DIR__ . '/../views/layout/header.php'; require __DIR__ . '/../views/products/form.php'; require __DIR__ . '/../views/layout/footer.php'; } public function store(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } Csrf::verify(); $pdo = DB::pdo(); $code = strtoupper(trim((string)($_POST['code'] ?? ''))); $lob_id = (int)($_POST['lob_id'] ?? 0); $desc = trim((string)($_POST['description'] ?? '')); if ($code === '' || $lob_id <= 0) { flash('error', 'Product code and line of business are required.'); redirect('/products/create'); } $chk = $pdo->prepare("SELECT 1 FROM products WHERE code=:c LIMIT 1"); $chk->execute(['c' => $code]); if ($chk->fetch()) { flash('error', 'This code already exists, please change it.'); redirect('/products/create'); } // Word template $tName = null; $tMime = null; $tData = null; if (!empty($_FILES['template']['name'])) { if ($_FILES['template']['error'] !== UPLOAD_ERR_OK) { flash('error', 'Template upload failed.'); redirect('/products/create'); } $tName = (string)$_FILES['template']['name']; $tMime = (string)($_FILES['template']['type'] ?? ''); $ext = strtolower(pathinfo($tName, PATHINFO_EXTENSION)); if (!in_array($ext, ['doc','docx'], true)) { flash('error', 'Template must be .doc or .docx.'); redirect('/products/create'); } if ((int)$_FILES['template']['size'] > 5 * 1024 * 1024) { flash('error', 'Template too large (max 5MB).'); redirect('/products/create'); } $tData = file_get_contents($_FILES['template']['tmp_name']); } // Excel $eName = null; $eMime = null; $eData = null; if (!empty($_FILES['excel']['name'])) { if ($_FILES['excel']['error'] !== UPLOAD_ERR_OK) { flash('error', 'Excel upload failed.'); redirect('/products/create'); } $eName = (string)$_FILES['excel']['name']; $eMime = (string)($_FILES['excel']['type'] ?? ''); $ext = strtolower(pathinfo($eName, PATHINFO_EXTENSION)); if (!in_array($ext, ['xls','xlsx'], true)) { flash('error', 'Excel must be .xls or .xlsx.'); redirect('/products/create'); } if ((int)$_FILES['excel']['size'] > 5 * 1024 * 1024) { flash('error', 'Excel too large (max 5MB).'); redirect('/products/create'); } $eData = file_get_contents($_FILES['excel']['tmp_name']); } $stmt = $pdo->prepare(" INSERT INTO products (code, lob_id, description, template_name, template_mime, template_data, excel_name, excel_mime, excel_data) VALUES (:code, :lob_id, :description, :tname, :tmime, :tdata, :ename, :emime, :edata) "); $stmt->execute([ 'code' => $code, 'lob_id' => $lob_id, 'description' => $desc !== '' ? $desc : null, 'tname' => $tName, 'tmime' => $tMime, 'tdata' => $tData, 'ename' => $eName, 'emime' => $eMime, 'edata' => $eData, ]); flash('success', 'Product created. Now you can add its form.'); redirect('/products'); } public function edit(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } $pdo = DB::pdo(); $id = (int)($_GET['id'] ?? 0); $stmt = $pdo->prepare("SELECT * FROM products WHERE id=:id"); $stmt->execute(['id' => $id]); $product = $stmt->fetch(); if (!$product) { flash('error', 'Product not found.'); redirect('/products'); } $lobs = $pdo->query("SELECT id, code, name FROM line_of_business ORDER BY name ASC")->fetchAll(); require __DIR__ . '/../views/layout/header.php'; require __DIR__ . '/../views/products/form.php'; require __DIR__ . '/../views/layout/footer.php'; } public function update(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } Csrf::verify(); $pdo = DB::pdo(); $id = (int)($_POST['id'] ?? 0); $code = strtoupper(trim((string)($_POST['code'] ?? ''))); $lob_id = (int)($_POST['lob_id'] ?? 0); $desc = trim((string)($_POST['description'] ?? '')); if ($id <= 0 || $code === '' || $lob_id <= 0) { flash('error', 'Product code and line of business are required.'); redirect('/products/edit?id=' . $id); } $chk = $pdo->prepare("SELECT 1 FROM products WHERE code=:c AND id<>:id LIMIT 1"); $chk->execute(['c' => $code, 'id' => $id]); if ($chk->fetch()) { flash('error', 'This code already exists, please change it.'); redirect('/products/edit?id=' . $id); } $setTemplate = ""; $setExcel = ""; $params = [ 'code' => $code, 'lob_id' => $lob_id, 'description' => $desc !== '' ? $desc : null, 'id' => $id ]; if (!empty($_FILES['template']['name'])) { if ($_FILES['template']['error'] !== UPLOAD_ERR_OK) { flash('error', 'Template upload failed.'); redirect('/products/edit?id=' . $id); } $tName = (string)$_FILES['template']['name']; $tMime = (string)($_FILES['template']['type'] ?? ''); $ext = strtolower(pathinfo($tName, PATHINFO_EXTENSION)); if (!in_array($ext, ['doc','docx'], true)) { flash('error', 'Template must be .doc or .docx.'); redirect('/products/edit?id=' . $id); } $tData = file_get_contents($_FILES['template']['tmp_name']); $setTemplate = ", template_name=:tname, template_mime=:tmime, template_data=:tdata"; $params['tname'] = $tName; $params['tmime'] = $tMime; $params['tdata'] = $tData; } if (!empty($_FILES['excel']['name'])) { if ($_FILES['excel']['error'] !== UPLOAD_ERR_OK) { flash('error', 'Excel upload failed.'); redirect('/products/edit?id=' . $id); } $eName = (string)$_FILES['excel']['name']; $eMime = (string)($_FILES['excel']['type'] ?? ''); $ext = strtolower(pathinfo($eName, PATHINFO_EXTENSION)); if (!in_array($ext, ['xls','xlsx'], true)) { flash('error', 'Excel must be .xls or .xlsx.'); redirect('/products/edit?id=' . $id); } $eData = file_get_contents($_FILES['excel']['tmp_name']); $setExcel = ", excel_name=:ename, excel_mime=:emime, excel_data=:edata"; $params['ename'] = $eName; $params['emime'] = $eMime; $params['edata'] = $eData; } $stmt = $pdo->prepare(" UPDATE products SET code=:code, lob_id=:lob_id, description=:description $setTemplate $setExcel WHERE id=:id "); $stmt->execute($params); flash('success', 'Product updated.'); redirect('/products'); } public function delete(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } $pdo = DB::pdo(); $id = (int)($_GET['id'] ?? 0); $stmt = $pdo->prepare("DELETE FROM products WHERE id=:id"); $stmt->execute(['id' => $id]); flash('success', 'Product deleted.'); redirect('/products'); } public function downloadTemplate(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } $pdo = DB::pdo(); $id = (int)($_GET['id'] ?? 0); $stmt = $pdo->prepare("SELECT template_name, template_mime, template_data FROM products WHERE id=:id LIMIT 1"); $stmt->execute(['id' => $id]); $row = $stmt->fetch(); if (!$row || empty($row['template_data'])) { http_response_code(404); echo "Template not found."; exit; } $name = $row['template_name'] ?: ('template_' . $id . '.docx'); $mime = $row['template_mime'] ?: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; header('Content-Type: ' . $mime); header('Content-Disposition: attachment; filename="' . $name . '"'); echo $row['template_data']; exit; } public function downloadExcel(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } $pdo = DB::pdo(); $id = (int)($_GET['id'] ?? 0); $stmt = $pdo->prepare("SELECT excel_name, excel_mime, excel_data FROM products WHERE id=:id LIMIT 1"); $stmt->execute(['id' => $id]); $row = $stmt->fetch(); if (!$row || empty($row['excel_data'])) { http_response_code(404); echo "Excel not found."; exit; } $name = $row['excel_name'] ?: ('excel_' . $id . '.xlsx'); $mime = $row['excel_mime'] ?: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; header('Content-Type: ' . $mime); header('Content-Disposition: attachment; filename="' . $name . '"'); echo $row['excel_data']; exit; } // ========================= // PRODUCT FORM BUILDER // ========================= public function formBuilder(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } $pdo = DB::pdo(); $product_id = (int)($_GET['id'] ?? 0); if ($product_id <= 0) redirect('/products'); $stmt = $pdo->prepare("SELECT * FROM products WHERE id=:id"); $stmt->execute(['id' => $product_id]); $product = $stmt->fetch(); if (!$product) { flash('error','Product not found'); redirect('/products'); } $fields = $pdo->prepare("SELECT * FROM product_form_fields WHERE product_id=:pid ORDER BY sort_order ASC, id ASC"); $fields->execute(['pid' => $product_id]); $fields = $fields->fetchAll(); require __DIR__ . '/../views/layout/header.php'; require __DIR__ . '/../views/products/form_builder.php'; require __DIR__ . '/../views/layout/footer.php'; } public function storeField(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } Csrf::verify(); $pdo = DB::pdo(); $product_id = (int)($_POST['product_id'] ?? 0); $label = trim((string)($_POST['field_label'] ?? '')); $type = (string)($_POST['field_type'] ?? 'yesno'); $opts = trim((string)($_POST['field_options'] ?? '')); $req = (int)($_POST['is_required'] ?? 1); $sort = (int)($_POST['sort_order'] ?? 0); if ($product_id <= 0 || $label === '') { flash('error','Field label is required.'); redirect('/products/formbuilder?id=' . $product_id); } if (!in_array($type, ['yesno','text','select'], true)) $type = 'yesno'; if ($type !== 'select') $opts = null; $stmt = $pdo->prepare(" INSERT INTO product_form_fields (product_id, field_label, field_type, field_options, is_required, sort_order) VALUES (:pid, :lbl, :typ, :opt, :req, :sort) "); $stmt->execute([ 'pid' => $product_id, 'lbl' => $label, 'typ' => $type, 'opt' => $opts, 'req' => $req ? 1 : 0, 'sort' => $sort ]); flash('success','Field added.'); redirect('/products/formbuilder?id=' . $product_id); } public function updateField(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } Csrf::verify(); $pdo = DB::pdo(); $id = (int)($_POST['id'] ?? 0); $product_id = (int)($_POST['product_id'] ?? 0); $label = trim((string)($_POST['field_label'] ?? '')); $type = (string)($_POST['field_type'] ?? 'yesno'); $opts = trim((string)($_POST['field_options'] ?? '')); $req = (int)($_POST['is_required'] ?? 1); $sort = (int)($_POST['sort_order'] ?? 0); if ($id <= 0 || $product_id <= 0 || $label === '') { flash('error','Invalid field data.'); redirect('/products/formbuilder?id=' . $product_id); } if (!in_array($type, ['yesno','text','select'], true)) $type = 'yesno'; if ($type !== 'select') $opts = null; $stmt = $pdo->prepare(" UPDATE product_form_fields SET field_label=:lbl, field_type=:typ, field_options=:opt, is_required=:req, sort_order=:sort WHERE id=:id AND product_id=:pid "); $stmt->execute([ 'lbl' => $label, 'typ' => $type, 'opt' => $opts, 'req' => $req ? 1 : 0, 'sort' => $sort, 'id' => $id, 'pid' => $product_id ]); flash('success','Field updated.'); redirect('/products/formbuilder?id=' . $product_id); } public function deleteField(): void { Auth::requireLogin(); if (!$this->canManage()) { http_response_code(403); echo "403 Forbidden"; exit; } $pdo = DB::pdo(); $id = (int)($_GET['id'] ?? 0); $pid = (int)($_GET['pid'] ?? 0); if ($id > 0) { $stmt = $pdo->prepare("DELETE FROM product_form_fields WHERE id=:id"); $stmt->execute(['id' => $id]); flash('success','Field deleted.'); } redirect('/products/formbuilder?id=' . $pid); } }