Product: FILE uploads
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# App
|
# App
|
||||||
app/config.php
|
app/config.php
|
||||||
|
app/uploads
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
|||||||
+13
-5
@@ -326,6 +326,19 @@ function ajax_request(options) {
|
|||||||
|
|
||||||
options.data = { json: JSON.stringify(payloadJson) };
|
options.data = { json: JSON.stringify(payloadJson) };
|
||||||
|
|
||||||
|
if (options.debugMode) {
|
||||||
|
isAjaxProcessing = false;
|
||||||
|
// Show FormData contents if applicable
|
||||||
|
if (options.formData instanceof FormData) {
|
||||||
|
// Log original formData before merging
|
||||||
|
for (let [key, value] of options.formData.entries()) {
|
||||||
|
console.log("FORMDATA: " + key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Show stringified JSON payload
|
||||||
|
console.log("REQUEST DATA:", options.data);
|
||||||
|
}
|
||||||
|
|
||||||
// IF formData exist, we pass as $_POST [not json]
|
// IF formData exist, we pass as $_POST [not json]
|
||||||
if (options.formData instanceof FormData) {
|
if (options.formData instanceof FormData) {
|
||||||
// THE BYPASS: If formData exists, move all text data into it
|
// THE BYPASS: If formData exists, move all text data into it
|
||||||
@@ -340,11 +353,6 @@ function ajax_request(options) {
|
|||||||
// --- START MODIFIED $.AJAX BLOCK ---
|
// --- START MODIFIED $.AJAX BLOCK ---
|
||||||
let isSendingFiles = (options.data instanceof FormData);
|
let isSendingFiles = (options.data instanceof FormData);
|
||||||
|
|
||||||
if (options.debugMode) {
|
|
||||||
isAjaxProcessing = false;
|
|
||||||
console.log("REQUEST DATA:", options.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show loading overlay
|
// Show loading overlay
|
||||||
if (options.noLoading !== true) {
|
if (options.noLoading !== true) {
|
||||||
$.LoadingOverlay("show", {
|
$.LoadingOverlay("show", {
|
||||||
|
|||||||
@@ -289,10 +289,12 @@ class FileUploader {
|
|||||||
|
|
||||||
private function ensureDirectory() {
|
private function ensureDirectory() {
|
||||||
if (!is_dir($this->target_dir)) {
|
if (!is_dir($this->target_dir)) {
|
||||||
mkdir($this->target_dir, 0755, true);
|
if (!mkdir($this->target_dir, 0755, true)) {
|
||||||
|
throw new Exception("Failed to create directory: " . $this->target_dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!is_writable($this->target_dir)) {
|
if (!is_writable($this->target_dir)) {
|
||||||
throw new Exception("Target directory is not writable.");
|
throw new Exception("Target directory is not writable: " . $this->target_dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,16 +327,25 @@ class FileUploader {
|
|||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
foreach ($_FILES[$field_name]['name'] as $key => $name) {
|
// Normalize single file to array structure
|
||||||
$error_code = $_FILES[$field_name]['error'][$key];
|
$files = $_FILES[$field_name];
|
||||||
|
if (!is_array($files['name'])) {
|
||||||
|
$files['name'] = [$files['name']];
|
||||||
|
$files['tmp_name'] = [$files['tmp_name']];
|
||||||
|
$files['error'] = [$files['error']];
|
||||||
|
$files['size'] = [$files['size']];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($files['name'] as $key => $name) {
|
||||||
|
$error_code = $files['error'][$key];
|
||||||
|
|
||||||
if ($error_code !== UPLOAD_ERR_OK) {
|
if ($error_code !== UPLOAD_ERR_OK) {
|
||||||
$errors[] = "{$name}: " . $this->getUploadError($error_code);
|
$errors[] = "{$name}: " . $this->getUploadError($error_code);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmp_name = $_FILES[$field_name]['tmp_name'][$key];
|
$tmp_name = $files['tmp_name'][$key];
|
||||||
$file_size = $_FILES[$field_name]['size'][$key];
|
$file_size = $files['size'][$key];
|
||||||
$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
if ($file_size > $this->max_size) {
|
if ($file_size > $this->max_size) {
|
||||||
@@ -356,7 +367,11 @@ class FileUploader {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$file_id = uniqid() . "_" . time() . "." . $extension;
|
// FILE NAME SANITIZATION + UNIQUE ID
|
||||||
|
$original = pathinfo($name, PATHINFO_FILENAME);
|
||||||
|
$original = preg_replace('/[^a-zA-Z0-9_-]/', '_', $original); // sanitize
|
||||||
|
$file_id = $original . "_" . uniqid() . "." . $extension;
|
||||||
|
|
||||||
$destination = $this->target_dir . $file_id;
|
$destination = $this->target_dir . $file_id;
|
||||||
|
|
||||||
if (move_uploaded_file($tmp_name, $destination)) {
|
if (move_uploaded_file($tmp_name, $destination)) {
|
||||||
@@ -379,6 +394,13 @@ class FileUploader {
|
|||||||
return implode(",", array_filter($final));
|
return implode(",", array_filter($final));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build final CSV string for DB as single file
|
||||||
|
*/
|
||||||
|
public function getUploadedFiles() {
|
||||||
|
return $this->uploaded_files;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rollback uploaded files if DB fails
|
* Rollback uploaded files if DB fails
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
|
|
||||||
// Cleanup deleted files
|
// Cleanup deleted files
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
$sql = "SELECT `files` FROM md_contact WHERE company_id = :company_id AND id = :id";
|
$sql = "SELECT `contact_image` FROM md_contact WHERE company_id = :company_id AND id = :id";
|
||||||
$sth = $pdo2->prepare($sql);
|
$sth = $pdo2->prepare($sql);
|
||||||
$sth->execute([":company_id" => $company_id, ":id" => $id]);
|
$sth->execute([":company_id" => $company_id, ":id" => $id]);
|
||||||
$uploader->cleanup($sth->fetchColumn(), $data['keep_files'] ?? '');
|
$uploader->cleanup($sth->fetchColumn(), $data['keep_files'] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload new files
|
// Upload new files
|
||||||
$errors = $uploader->upload('contact_files');
|
$errors = $uploader->upload('contact_image');
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
$answer["success"] = 0;
|
$answer["success"] = 0;
|
||||||
$answer["message"] = $errors[0];
|
$answer["message"] = $errors[0];
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
":shipping_location"=> $data["shipping_location"],
|
":shipping_location"=> $data["shipping_location"],
|
||||||
":shipping_address" => $data["shipping_address"],
|
":shipping_address" => $data["shipping_address"],
|
||||||
":remark" => $data["remark"],
|
":remark" => $data["remark"],
|
||||||
":files" => $db_image_string,
|
":contact_image" => $db_image_string,
|
||||||
":status" => (int)$data["status"],
|
":status" => (int)$data["status"],
|
||||||
":log" => json_encode($table_log),
|
":log" => json_encode($table_log),
|
||||||
];
|
];
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
`shipping_location` = :shipping_location,
|
`shipping_location` = :shipping_location,
|
||||||
`shipping_address` = :shipping_address,
|
`shipping_address` = :shipping_address,
|
||||||
`remark` = :remark,
|
`remark` = :remark,
|
||||||
`files` = :files,
|
`contact_image` = :contact_image,
|
||||||
`status` = :status,
|
`status` = :status,
|
||||||
`log` = :log
|
`log` = :log
|
||||||
WHERE id = :id AND company_id = :company_id";
|
WHERE id = :id AND company_id = :company_id";
|
||||||
@@ -76,10 +76,10 @@
|
|||||||
} else {
|
} else {
|
||||||
$sql = "INSERT INTO md_contact
|
$sql = "INSERT INTO md_contact
|
||||||
(company_id, contact_name, tax_id, organization, branch, contact_type,
|
(company_id, contact_name, tax_id, organization, branch, contact_type,
|
||||||
billing_address, shipping_location, shipping_address, remark, files, status, log)
|
billing_address, shipping_location, shipping_address, remark, contact_image, status, log)
|
||||||
VALUES
|
VALUES
|
||||||
(:company_id, :contact_name, :tax_id, :organization, :branch, :contact_type,
|
(:company_id, :contact_name, :tax_id, :organization, :branch, :contact_type,
|
||||||
:billing_address, :shipping_location, :shipping_address, :remark, :files, :status, :log)";
|
:billing_address, :shipping_location, :shipping_address, :remark, :contact_image, :status, :log)";
|
||||||
}
|
}
|
||||||
|
|
||||||
$sth = $pdo->prepare($sql);
|
$sth = $pdo->prepare($sql);
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
parallelUploads: 10,
|
parallelUploads: 10,
|
||||||
acceptedFiles: "image/*",
|
acceptedFiles: "image/*",
|
||||||
addRemoveLinks: true,
|
addRemoveLinks: true,
|
||||||
paramName: "contact_files"
|
paramName: "contact_image"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,8 +168,8 @@
|
|||||||
let files = myDropzone.getQueuedFiles();
|
let files = myDropzone.getQueuedFiles();
|
||||||
|
|
||||||
files.forEach(function(file) {
|
files.forEach(function(file) {
|
||||||
// Use 'contact_files[]' so PHP creates an array in $_FILES
|
// Use 'contact_image[]' so PHP creates an array in $_FILES
|
||||||
formData.append('contact_files[]', file);
|
formData.append('contact_image[]', file);
|
||||||
})
|
})
|
||||||
|
|
||||||
let keepFiles = [];
|
let keepFiles = [];
|
||||||
@@ -237,8 +237,8 @@
|
|||||||
|
|
||||||
let item = res.output;
|
let item = res.output;
|
||||||
// 2. Handle files specifically (No loop needed for the row)
|
// 2. Handle files specifically (No loop needed for the row)
|
||||||
if (item.files) {
|
if (item.contact_image) {
|
||||||
let fileArray = item.files.split(',');
|
let fileArray = item.contact_image.split(',');
|
||||||
|
|
||||||
fileArray.forEach(function(fileName) {
|
fileArray.forEach(function(fileName) {
|
||||||
if (fileName.trim() !== "") {
|
if (fileName.trim() !== "") {
|
||||||
|
|||||||
@@ -3,11 +3,40 @@
|
|||||||
|
|
||||||
require '../../../assets/utils/db_auth.php';
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
|
||||||
|
$id = (int)$data['id'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) {
|
/** 1. FILE HANDLING **/
|
||||||
|
$uploader = new FileUploader($include_url . "uploads/product/");
|
||||||
|
|
||||||
$id = (int)$data['id'];
|
if (!empty($_FILES['product_image']['name'])) {
|
||||||
|
// Cleanup old image when updating
|
||||||
|
if ($id > 0) {
|
||||||
|
$sql = "SELECT `product_image` FROM md_product WHERE company_id = :company_id AND id = :id";
|
||||||
|
$sth = $pdo2->prepare($sql);
|
||||||
|
$sth->execute([":company_id" => $company_id, ":id" => $id]);
|
||||||
|
$existing_files = $sth->fetchColumn();
|
||||||
|
|
||||||
|
$uploader->cleanup($existing_files, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = $uploader->upload('product_image');
|
||||||
|
if (!empty($errors)) {
|
||||||
|
$answer["success"] = 0;
|
||||||
|
$answer["message"] = $errors[0];
|
||||||
|
exit(json_encode($answer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the single uploaded filename directly
|
||||||
|
$db_image_string = $uploader->getUploadedFiles()[0] ?? '';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// No new file — keep existing, skip cleanup entirely
|
||||||
|
$db_image_string = $data['keep_files'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $id, $db_image_string) {
|
||||||
|
|
||||||
// get existing log
|
// get existing log
|
||||||
$sql = "SELECT `log` FROM md_product
|
$sql = "SELECT `log` FROM md_product
|
||||||
@@ -44,7 +73,7 @@
|
|||||||
":price" => $data["price"],
|
":price" => $data["price"],
|
||||||
":min_stock" => $data["min_stock"],
|
":min_stock" => $data["min_stock"],
|
||||||
":category" => $data["category"],
|
":category" => $data["category"],
|
||||||
":product_image"=> $data["product_image"],
|
":product_image"=> $db_image_string,
|
||||||
":description" => $data["description"],
|
":description" => $data["description"],
|
||||||
":status" => $data["status"],
|
":status" => $data["status"],
|
||||||
":log" => json_encode($table_log),
|
":log" => json_encode($table_log),
|
||||||
@@ -65,7 +94,7 @@
|
|||||||
":price" => $data["price"],
|
":price" => $data["price"],
|
||||||
":min_stock" => $data["min_stock"],
|
":min_stock" => $data["min_stock"],
|
||||||
":category" => $data["category"],
|
":category" => $data["category"],
|
||||||
":product_image"=> $data["product_image"],
|
":product_image"=> $db_image_string,
|
||||||
":description" => $data["description"],
|
":description" => $data["description"],
|
||||||
":status" => $data["status"],
|
":status" => $data["status"],
|
||||||
":log" => json_encode($table_log),
|
":log" => json_encode($table_log),
|
||||||
@@ -83,6 +112,7 @@
|
|||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$answer["success"] = 0;
|
$answer["success"] = 0;
|
||||||
$answer["message"] = "Something went wrong";
|
$answer["message"] = "Something went wrong";
|
||||||
|
$answer["error"] = $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
|
|||||||
@@ -49,21 +49,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
<label for="category" class="form-label">Category</label>
|
<label for="category" class="form-label">Category</label>
|
||||||
<select class="form-select" id="category" required> </select>
|
<select class="form-select" id="category" required> </select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<!-- Remove d-none from file input so it shows by default -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="product_image" class="form-label">Product Image</label>
|
<label class="form-label">Product Image</label>
|
||||||
|
<div id="current_image_display" class="d-none">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class='input-group'>
|
||||||
|
<span class='btn btn-outline-secondary' type='button' id='change_image_btn'>Change</span>
|
||||||
|
<input type='text' class='form-control' id='current_file_label' disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<input type="file" class="form-control" id="product_image" accept="image/*">
|
<input type="file" class="form-control" id="product_image" accept="image/*">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="description" class="form-label">Description</label>
|
<label for="description" class="form-label">Description</label>
|
||||||
<textarea class="form-control" id="description" rows="4"
|
<textarea class="form-control" id="description" rows="4"
|
||||||
placeholder="Enter product description"></textarea>
|
placeholder="Enter product description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary" onclick="add_product();">Create</button>
|
<button type="submit" class="btn btn-primary" onclick="manage_product();">Create</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,13 +93,24 @@
|
|||||||
<?php require "../include_ending.php";?>
|
<?php require "../include_ending.php";?>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function add_product() {
|
function manage_product() {
|
||||||
|
|
||||||
|
let formData = new FormData();
|
||||||
|
|
||||||
|
// Append the file (single image, not array)
|
||||||
|
let imageFile = $('#product_image')[0].files[0];
|
||||||
|
if (imageFile) {
|
||||||
|
formData.append('product_image', imageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append('keep_files', $('#current_file_label').val())
|
||||||
|
|
||||||
return ajax_request({
|
return ajax_request({
|
||||||
url: "<?php echo $server_url?>inventory/api/engine/manage_product.php",
|
url: "<?php echo $server_url?>inventory/api/engine/manage_product.php",
|
||||||
autoPrepare: true,
|
autoPrepare: true,
|
||||||
checkRequired: 0,
|
checkRequired: 0,
|
||||||
debugMode: 0,
|
debugMode: 1,
|
||||||
|
formData: formData,
|
||||||
action: 'manage',
|
action: 'manage',
|
||||||
onSuccess: function(res) {
|
onSuccess: function(res) {
|
||||||
|
|
||||||
@@ -133,7 +156,21 @@
|
|||||||
onSuccess: function(res) {
|
onSuccess: function(res) {
|
||||||
|
|
||||||
$.each(res.output, function(key, item) {
|
$.each(res.output, function(key, item) {
|
||||||
$(`#${key}`).val(item);
|
let el = $(`#${key}`);
|
||||||
|
if (el.attr('type') === 'file') return; // skip file inputs
|
||||||
|
el.val(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle image field
|
||||||
|
if (res.output.product_image) {
|
||||||
|
$('#current_file_label').val(res.output.product_image);
|
||||||
|
$('#current_image_display').removeClass('d-none'); // show filename + change btn
|
||||||
|
$('#product_image').addClass('d-none'); // hide file input
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#change_image_btn').on('click', function() {
|
||||||
|
$('#current_image_display').addClass('d-none');
|
||||||
|
$('#product_image').removeClass('d-none');
|
||||||
});
|
});
|
||||||
|
|
||||||
$(`button[type=submit]`).text('Update');
|
$(`button[type=submit]`).text('Update');
|
||||||
|
|||||||
Reference in New Issue
Block a user