diff --git a/app.zip b/app.zip new file mode 100644 index 0000000..5cf4c6e Binary files /dev/null and b/app.zip differ diff --git a/app/assets/utils/classes/FileUploader.php b/app/assets/utils/classes/FileUploader.php new file mode 100644 index 0000000..419f189 --- /dev/null +++ b/app/assets/utils/classes/FileUploader.php @@ -0,0 +1,162 @@ +target_dir = rtrim($target_dir, '/') . '/'; + $this->ensureDirectory(); + } + + private function ensureDirectory() { + if (!is_dir($this->target_dir)) { + if (!mkdir($this->target_dir, 0755, true)) { + throw new Exception("Failed to create directory: " . $this->target_dir); + } + } + if (!is_writable($this->target_dir)) { + throw new Exception("Target directory is not writable: " . $this->target_dir); + } + } + + /** + * Remove files that user deleted in the UI + */ + public function cleanup($existing_csv, $keep_csv) { + if (empty($existing_csv)) return; + + $existing = array_filter(array_map('trim', explode(',', $existing_csv))); + $keep = !empty($keep_csv) ? array_filter(array_map('trim', explode(',', $keep_csv))) : []; + + foreach ($existing as $file) { + if (!in_array($file, $keep)) { + $path = $this->target_dir . $file; + if (file_exists($path)) { + unlink($path); + $this->deleted_files[] = $file; + } + } + } + } + + /** + * Upload new files from $_FILES + * Returns array of errors (empty = success) + */ + public function upload($field_name) { + if (!isset($_FILES[$field_name])) return []; + + $errors = []; + + // Normalize single file to array structure + $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) { + $errors[] = "{$name}: " . $this->getUploadError($error_code); + continue; + } + + $tmp_name = $files['tmp_name'][$key]; + $file_size = $files['size'][$key]; + $extension = strtolower(pathinfo($name, PATHINFO_EXTENSION)); + + if ($file_size > $this->max_size) { + $errors[] = "{$name}: exceeds " . ($this->max_size / 1024 / 1024) . "MB limit"; + continue; + } + + if (!in_array($extension, $this->allowed_extensions)) { + $errors[] = "{$name}: .{$extension} is not allowed"; + continue; + } + + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $tmp_name); + finfo_close($finfo); + + if (!in_array($mime, $this->allowed_mimes)) { + $errors[] = "{$name}: content type ({$mime}) is not allowed"; + continue; + } + + // 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; + + if (move_uploaded_file($tmp_name, $destination)) { + $this->uploaded_files[] = $file_id; + chmod($destination, 0644); + } else { + $errors[] = "{$name}: failed to save"; + } + } + + return $errors; + } + + /** + * Build final CSV string for DB + */ + public function buildFileString($keep_csv) { + $keep = !empty($keep_csv) ? array_filter(array_map('trim', explode(',', $keep_csv))) : []; + $final = array_merge($keep, $this->uploaded_files); + 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 + */ + public function rollbackUploads() { + foreach ($this->uploaded_files as $file) { + $path = $this->target_dir . $file; + if (file_exists($path)) { + unlink($path); + } + } + $this->uploaded_files = []; + } + + private function getUploadError($code) { + $messages = [ + UPLOAD_ERR_INI_SIZE => "exceeds server max upload size (" . ini_get('upload_max_filesize') . ")", + UPLOAD_ERR_FORM_SIZE => "exceeds form max size", + UPLOAD_ERR_PARTIAL => "was only partially uploaded", + UPLOAD_ERR_NO_FILE => "no file was sent", + UPLOAD_ERR_NO_TMP_DIR => "server missing temp folder", + UPLOAD_ERR_CANT_WRITE => "server failed to write to disk", + UPLOAD_ERR_EXTENSION => "blocked by server extension", + ]; + return $messages[$code] ?? "unknown error (code {$code})"; + } +} + + + + + +?> \ No newline at end of file diff --git a/app/assets/utils/classes/Reports.php b/app/assets/utils/classes/Reports.php new file mode 100644 index 0000000..65dc777 --- /dev/null +++ b/app/assets/utils/classes/Reports.php @@ -0,0 +1,121 @@ +pdo = $pdo; + $this->companyId = $companyId; + } + + public function getTotalCategory(): int + { + $sql = "SELECT COUNT(*) + FROM md_product_category + WHERE company_id = :company_id"; + return (int) $this->fetchScalar($sql); + } + + public function getActiveCategory(): int + { + $sql = "SELECT COUNT(*) + FROM md_product_category + WHERE company_id = :company_id + AND `status` = 1"; + return (int) $this->fetchScalar($sql); + } + + public function getInactiveCategory(): int + { + $sql = "SELECT COUNT(*) + FROM md_product_category + WHERE company_id = :company_id + AND `status` = 0"; + return (int) $this->fetchScalar($sql); + } + + public function getTotalWarehouse(): int + { + $sql = "SELECT COUNT(*) + FROM md_warehouse + WHERE company_id = :company_id"; + return (int) $this->fetchScalar($sql); + } + + public function getTotalLocation(): int + { + $sql = "SELECT COUNT(DISTINCT `location`) + FROM md_warehouse + WHERE company_id = :company_id"; + return (int) $this->fetchScalar($sql); + } + + public function getTotalCapacity(): float + { + $sql = "SELECT SUM(capacity) + FROM md_warehouse + WHERE company_id = :company_id"; + return (float) $this->fetchScalar($sql); + } + + public function getTotalProduct(): int + { + $sql = "SELECT COUNT(*) + FROM md_product + WHERE company_id = :company_id"; + return (int) $this->fetchScalar($sql); + } + + public function getLowStockCount(): int + { + $products = $this->getStockBalance(); + $count = 0; + foreach ($products as $product) { + $balance = (float) $product["total_in"] - (float) $product["total_out"]; + if ($balance < (float) $product["min_stock"]) { + $count++; + } + } + return $count; + } + + public function getStockBalance(): array + { + $sql = "SELECT + a.product_sku, + SUM(a.total_in) AS total_in, + SUM(a.total_out) AS total_out, + b.min_stock + FROM warehouse_balance a + LEFT JOIN md_product b + ON a.company_id = b.company_id + AND a.product_sku = b.sku + WHERE a.company_id = :company_id + GROUP BY a.product_sku, b.min_stock"; + return $this->fetchAll($sql); + } + + // FETCH HELPERS + // These helper methods abstract away the common pattern of preparing, executing, and fetching results from the database. + private function fetchScalar(string $sql) + { + $sth = $this->pdo->prepare($sql); + $sth->execute([":company_id" => $this->companyId]); + return $sth->fetchColumn(); + } + + private function fetchAll(string $sql): array + { + $sth = $this->pdo->prepare($sql); + $sth->execute([":company_id" => $this->companyId]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } +} + + + + +?> \ No newline at end of file diff --git a/app/assets/utils/classes/WarehouseManager.php b/app/assets/utils/classes/WarehouseManager.php new file mode 100644 index 0000000..97604f5 --- /dev/null +++ b/app/assets/utils/classes/WarehouseManager.php @@ -0,0 +1,105 @@ +pdo = $pdo; + $this->company_id = $company_id; + } + + public function getWarehouseName($warehouse_id) { + $sql = "SELECT warehouse_name FROM md_warehouse + WHERE company_id = :company_id AND id = :warehouse_id"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":warehouse_id" => $warehouse_id + ]); + return $sth->fetchColumn(); + } + + public function getStockContext($warehouse_id, $id) { + $name = $this->getWarehouseName($warehouse_id); + $table = "td_stock_" . $name; + + if (empty($id)) { + return [ + 'table' => $table, + 'name' => $name, + 'row' => [] + ]; + } + + $sql = "SELECT * FROM `$table` + WHERE company_id = :company_id AND id = :id"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":id" => $id + ]); + + return [ + 'table' => $table, + 'name' => $name, + 'row' => $sth->fetch(PDO::FETCH_ASSOC) ?: [] + ]; + } + + public function adjustBalance($type, $warehouse_id, $product_sku, $old_qty, $new_qty) { + + // Lock the row — other transactions wait here + $sql = "SELECT id FROM warehouse_balance + WHERE company_id = :company_id + AND warehouse_id = :warehouse_id + AND product_sku = :product_sku + FOR UPDATE"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":warehouse_id" => $warehouse_id, + ":product_sku" => $product_sku + ]); + + if (!$sth->fetchColumn()) { + $sql = "INSERT INTO warehouse_balance + (company_id, warehouse_id, product_sku) + VALUES (:company_id, :warehouse_id, :product_sku)"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":warehouse_id" => $warehouse_id, + ":product_sku" => $product_sku + ]); + } + + $column = $type === 'in' ? 'total_in' : 'total_out'; + + // This is already atomic since it uses relative update + $sql = "UPDATE warehouse_balance + SET `$column` = `$column` - :old_qty + :new_qty + WHERE company_id = :company_id + AND warehouse_id = :warehouse_id + AND product_sku = :product_sku"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":warehouse_id" => $warehouse_id, + ":product_sku" => $product_sku, + ":old_qty" => $old_qty, + ":new_qty" => $new_qty + ]); + } +} + + + + +?> \ No newline at end of file diff --git a/app/assets/utils/db_auth.php b/app/assets/utils/db_auth.php index febe96b..f343f9b 100644 --- a/app/assets/utils/db_auth.php +++ b/app/assets/utils/db_auth.php @@ -2,78 +2,7 @@ require_once __DIR__."/../../config.php"; require_once __DIR__."/../../dbconn.php"; - - -/** - * Check PDO statement for errors - * Logs full error server-side, returns generic message to client - * - * @param PDOStatement $sth - The executed PDO statement - * @param array &$answer - Response array passed by reference - * @param array $options - Optional settings: - * 'message' => Custom client-facing error message - * 'log' => Custom server log label - * 'code' => HTTP response code (default: 500) - * 'before_exit' => Callback function before exit - * - * @example - * // Simple — use all defaults - * db_check($sth, $answer); - * - * @example - * // Custom client message - * db_check($sth, $answer, [ - * 'message' => "Contact not found." - * ]); - * - * @example - * // Custom log label + message - * db_check($sth, $answer, [ - * 'log' => "Failed to fetch contact", - * 'message' => "Could not load contact. Please try again." - * ]); - * - * @example - * // Custom HTTP status code - * db_check($sth, $answer, [ - * 'code' => 403, - * 'message' => "Access denied." - * ]); - * - * @example - * // Cleanup callback before exit - * db_check($sth, $answer, [ - * 'message' => "Upload failed.", - * 'before_exit' => function() use ($tmp_file) { - * if(file_exists($tmp_file)) unlink($tmp_file); - * } - * ]); - */ -function db_check($sth, &$answer, $options = []) { - if ($sth->errorInfo()[0] != "00000" && !empty($sth->errorInfo()[0])) { - - $trace = debug_backtrace()[0]; - - $log_message = $options['log'] ?? "DB Error"; - error_log($log_message . " in " . $trace['file'] . " line " . $trace['line'] . ": " . $sth->errorInfo()[2]); - - // production version - $answer["message"] = $options['message'] ?? "A server error occurred. Please try again."; - - // development version - $answer["message"] = $sth->errorInfo()[2]; - - http_response_code($options['code'] ?? 500); - - if(isset($options['before_exit'])) { - call_user_func($options['before_exit']); - } - - exit(json_encode($answer)); - } -} - - +require_once __DIR__."/db_helpers.php"; if(!empty($_SESSION["login_company_id"])){ @@ -164,300 +93,4 @@ if(!empty($_SESSION["login_company_id"])){ } -/** - * HELPER CLASSES AND FUNCTIONS - * These are designed to be reusable across different API endpoints for consistent data handling and error management. - * They are not specific to any single operation and can be used wherever similar patterns arise. - */ - -/** - * Helper class to manage data consistency - */ -class WarehouseManager { - - private $pdo; - private $company_id; - - public function __construct($pdo, $company_id) { - $this->pdo = $pdo; - $this->company_id = $company_id; - } - - public function getWarehouseName($warehouse_id) { - $sql = "SELECT warehouse_name FROM md_warehouse - WHERE company_id = :company_id AND id = :warehouse_id"; - - $sth = $this->pdo->prepare($sql); - $sth->execute([ - ":company_id" => $this->company_id, - ":warehouse_id" => $warehouse_id - ]); - return $sth->fetchColumn(); - } - - public function getStockContext($warehouse_id, $id) { - $name = $this->getWarehouseName($warehouse_id); - $table = "td_stock_" . $name; - - if (empty($id)) { - return [ - 'table' => $table, - 'name' => $name, - 'row' => [] - ]; - } - - $sql = "SELECT * FROM `$table` - WHERE company_id = :company_id AND id = :id"; - - $sth = $this->pdo->prepare($sql); - $sth->execute([ - ":company_id" => $this->company_id, - ":id" => $id - ]); - - return [ - 'table' => $table, - 'name' => $name, - 'row' => $sth->fetch(PDO::FETCH_ASSOC) ?: [] - ]; - } - - public function adjustBalance($type, $warehouse_id, $product_sku, $old_qty, $new_qty) { - - // Lock the row — other transactions wait here - $sql = "SELECT id FROM warehouse_balance - WHERE company_id = :company_id - AND warehouse_id = :warehouse_id - AND product_sku = :product_sku - FOR UPDATE"; - - $sth = $this->pdo->prepare($sql); - $sth->execute([ - ":company_id" => $this->company_id, - ":warehouse_id" => $warehouse_id, - ":product_sku" => $product_sku - ]); - - if (!$sth->fetchColumn()) { - $sql = "INSERT INTO warehouse_balance - (company_id, warehouse_id, product_sku) - VALUES (:company_id, :warehouse_id, :product_sku)"; - - $sth = $this->pdo->prepare($sql); - $sth->execute([ - ":company_id" => $this->company_id, - ":warehouse_id" => $warehouse_id, - ":product_sku" => $product_sku - ]); - } - - $column = $type === 'in' ? 'total_in' : 'total_out'; - - // This is already atomic since it uses relative update - $sql = "UPDATE warehouse_balance - SET `$column` = `$column` - :old_qty + :new_qty - WHERE company_id = :company_id - AND warehouse_id = :warehouse_id - AND product_sku = :product_sku"; - - $sth = $this->pdo->prepare($sql); - $sth->execute([ - ":company_id" => $this->company_id, - ":warehouse_id" => $warehouse_id, - ":product_sku" => $product_sku, - ":old_qty" => $old_qty, - ":new_qty" => $new_qty - ]); - } -} - - -class FileUploader { - - private $target_dir; - private $allowed_mimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf']; - private $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf']; - private $max_size = 5 * 1024 * 1024; // 5MB - private $uploaded_files = []; // newly uploaded filenames - private $deleted_files = []; // files we deleted from disk - - public function __construct($target_dir) { - $this->target_dir = rtrim($target_dir, '/') . '/'; - $this->ensureDirectory(); - } - - private function ensureDirectory() { - if (!is_dir($this->target_dir)) { - if (!mkdir($this->target_dir, 0755, true)) { - throw new Exception("Failed to create directory: " . $this->target_dir); - } - } - if (!is_writable($this->target_dir)) { - throw new Exception("Target directory is not writable: " . $this->target_dir); - } - } - - /** - * Remove files that user deleted in the UI - */ - public function cleanup($existing_csv, $keep_csv) { - if (empty($existing_csv)) return; - - $existing = array_filter(array_map('trim', explode(',', $existing_csv))); - $keep = !empty($keep_csv) ? array_filter(array_map('trim', explode(',', $keep_csv))) : []; - - foreach ($existing as $file) { - if (!in_array($file, $keep)) { - $path = $this->target_dir . $file; - if (file_exists($path)) { - unlink($path); - $this->deleted_files[] = $file; - } - } - } - } - - /** - * Upload new files from $_FILES - * Returns array of errors (empty = success) - */ - public function upload($field_name) { - if (!isset($_FILES[$field_name])) return []; - - $errors = []; - - // Normalize single file to array structure - $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) { - $errors[] = "{$name}: " . $this->getUploadError($error_code); - continue; - } - - $tmp_name = $files['tmp_name'][$key]; - $file_size = $files['size'][$key]; - $extension = strtolower(pathinfo($name, PATHINFO_EXTENSION)); - - if ($file_size > $this->max_size) { - $errors[] = "{$name}: exceeds " . ($this->max_size / 1024 / 1024) . "MB limit"; - continue; - } - - if (!in_array($extension, $this->allowed_extensions)) { - $errors[] = "{$name}: .{$extension} is not allowed"; - continue; - } - - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mime = finfo_file($finfo, $tmp_name); - finfo_close($finfo); - - if (!in_array($mime, $this->allowed_mimes)) { - $errors[] = "{$name}: content type ({$mime}) is not allowed"; - continue; - } - - // 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; - - if (move_uploaded_file($tmp_name, $destination)) { - $this->uploaded_files[] = $file_id; - chmod($destination, 0644); - } else { - $errors[] = "{$name}: failed to save"; - } - } - - return $errors; - } - - /** - * Build final CSV string for DB - */ - public function buildFileString($keep_csv) { - $keep = !empty($keep_csv) ? array_filter(array_map('trim', explode(',', $keep_csv))) : []; - $final = array_merge($keep, $this->uploaded_files); - 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 - */ - public function rollbackUploads() { - foreach ($this->uploaded_files as $file) { - $path = $this->target_dir . $file; - if (file_exists($path)) { - unlink($path); - } - } - $this->uploaded_files = []; - } - - private function getUploadError($code) { - $messages = [ - UPLOAD_ERR_INI_SIZE => "exceeds server max upload size (" . ini_get('upload_max_filesize') . ")", - UPLOAD_ERR_FORM_SIZE => "exceeds form max size", - UPLOAD_ERR_PARTIAL => "was only partially uploaded", - UPLOAD_ERR_NO_FILE => "no file was sent", - UPLOAD_ERR_NO_TMP_DIR => "server missing temp folder", - UPLOAD_ERR_CANT_WRITE => "server failed to write to disk", - UPLOAD_ERR_EXTENSION => "blocked by server extension", - ]; - return $messages[$code] ?? "unknown error (code {$code})"; - } -} - - -/** - * Helper function - */ - -// transaction wrapper with retry logic for deadlocks -function dbTransaction($pdo, $callback, $maxRetries = 3) { - for ($attempt = 0; $attempt < $maxRetries; $attempt++) { - try { - $pdo->beginTransaction(); - $result = $callback($pdo); - $pdo->commit(); - return $result; - - } catch (PDOException $e) { - $pdo->rollBack(); - - if ($e->getCode() == '40001' && $attempt < $maxRetries - 1) { - usleep(600000 * ($attempt + 1)); - continue; - } - - error_log("[DB ERROR] " . $e->getMessage() . " | " . $e->getFile() . ":" . $e->getLine()); - throw $e; - - } catch (Exception $e) { - $pdo->rollBack(); - error_log("[ERROR] " . $e->getMessage() . " | " . $e->getFile() . ":" . $e->getLine()); - throw $e; - } - } -} ?> \ No newline at end of file diff --git a/app/assets/utils/db_helpers.php b/app/assets/utils/db_helpers.php new file mode 100644 index 0000000..3a058b0 --- /dev/null +++ b/app/assets/utils/db_helpers.php @@ -0,0 +1,100 @@ + Custom client-facing error message + * 'log' => Custom server log label + * 'code' => HTTP response code (default: 500) + * 'before_exit' => Callback function before exit + * + * @example + * // Simple — use all defaults + * db_check($sth, $answer); + * + * @example + * // Custom client message + * db_check($sth, $answer, [ + * 'message' => "Contact not found." + * ]); + * + * @example + * // Custom log label + message + * db_check($sth, $answer, [ + * 'log' => "Failed to fetch contact", + * 'message' => "Could not load contact. Please try again." + * ]); + * + * @example + * // Custom HTTP status code + * db_check($sth, $answer, [ + * 'code' => 403, + * 'message' => "Access denied." + * ]); + * + * @example + * // Cleanup callback before exit + * db_check($sth, $answer, [ + * 'message' => "Upload failed.", + * 'before_exit' => function() use ($tmp_file) { + * if(file_exists($tmp_file)) unlink($tmp_file); + * } + * ]); + */ + + +function db_check($sth, &$answer, $options = []) { + if ($sth->errorInfo()[0] != "00000" && !empty($sth->errorInfo()[0])) { + + $trace = debug_backtrace()[0]; + + $log_message = $options['log'] ?? "DB Error"; + error_log($log_message . " in " . $trace['file'] . " line " . $trace['line'] . ": " . $sth->errorInfo()[2]); + + // production version + $answer["message"] = $options['message'] ?? "A server error occurred. Please try again."; + + http_response_code($options['code'] ?? 500); + + if(isset($options['before_exit'])) { + call_user_func($options['before_exit']); + } + + exit(json_encode($answer)); + } +} + + + +// transaction wrapper with retry logic for deadlocks +function dbTransaction($pdo, $callback, $maxRetries = 3) { + for ($attempt = 0; $attempt < $maxRetries; $attempt++) { + try { + $pdo->beginTransaction(); + $result = $callback($pdo); + $pdo->commit(); + return $result; + + } catch (PDOException $e) { + $pdo->rollBack(); + + if ($e->getCode() == '40001' && $attempt < $maxRetries - 1) { + usleep(600000 * ($attempt + 1)); + continue; + } + + error_log("[DB ERROR] " . $e->getMessage() . " | " . $e->getFile() . ":" . $e->getLine()); + throw $e; + + } catch (Exception $e) { + $pdo->rollBack(); + error_log("[ERROR] " . $e->getMessage() . " | " . $e->getFile() . ":" . $e->getLine()); + throw $e; + } + } +} + +?> \ No newline at end of file diff --git a/app/contact/api/engine/manage_contact.php b/app/contact/api/engine/manage_contact.php index a74f540..17e112c 100644 --- a/app/contact/api/engine/manage_contact.php +++ b/app/contact/api/engine/manage_contact.php @@ -1,6 +1,7 @@ -
-
- -
- -
-
- Download Now -
- + + diff --git a/app/dashboard/reports.php b/app/dashboard/reports.php new file mode 100644 index 0000000..d022cb9 --- /dev/null +++ b/app/dashboard/reports.php @@ -0,0 +1,186 @@ + + + + + + + + +
+
+
+
+
+
+

Reports

+

View your inventory analytics and reports

+
+ +
+
+
+ +
+ +
+
+
+
Total Revenue
+

$45,231

+

12% from last month

+
+
+
+ +
+
+
+
Products Sold
+

1,234

+

8% from last month

+
+
+
+ +
+
+
+
Low Stock Items
+

23

+

3% from last month

+
+
+
+ +
+
+
+
Out of Stock
+

5

+

2% from last month

+
+
+
+
+ +
+ +
+
+
+
+
+

Sales Overview

+ +
+
+ + +
+
+ + + +
+ + + +
+
+
+
+ +
+ +
+
+
+
+
+

Top Products

+ +
+
+ + +
+
+
+ Product A +
+
+
+
+
Gaming Joy Stick
+ 156 units sold +
+
+ $3,120 +
+
+
+
+ +
+
+ Product B +
+
+
+
+
Wireless Headphones
+ 134 units sold +
+
+ $2,680 +
+
+
+
+ +
+
+ Product C +
+
+
+
+
Smartwatch
+ 98 units sold +
+
+ $1,960 +
+
+
+
+
+ +
+
+
+
+ +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/app/ics/api/engine/manage_stock_in.php b/app/ics/api/engine/manage_stock_in.php index 23a8cf5..3e63f15 100644 --- a/app/ics/api/engine/manage_stock_in.php +++ b/app/ics/api/engine/manage_stock_in.php @@ -2,6 +2,7 @@ session_start(); require '../../../assets/utils/db_auth.php'; + require '../../../assets/utils/classes/WarehouseManager.php'; try { diff --git a/app/ics/api/engine/manage_stock_out.php b/app/ics/api/engine/manage_stock_out.php index d7824ba..30406bd 100644 --- a/app/ics/api/engine/manage_stock_out.php +++ b/app/ics/api/engine/manage_stock_out.php @@ -2,6 +2,7 @@ session_start(); require '../../../assets/utils/db_auth.php'; + require '../../../assets/utils/classes/WarehouseManager.php'; try { diff --git a/app/ics/api/engine/manage_stock_transfer.php b/app/ics/api/engine/manage_stock_transfer.php index b37288b..7ab2d09 100644 --- a/app/ics/api/engine/manage_stock_transfer.php +++ b/app/ics/api/engine/manage_stock_transfer.php @@ -2,6 +2,7 @@ session_start(); require '../../../assets/utils/db_auth.php'; + require '../../../assets/utils/classes/WarehouseManager.php'; try { diff --git a/app/ics/api/engine_report/product_stats.php b/app/ics/api/engine_report/product_stats.php index f3a52d5..eb76ff3 100644 --- a/app/ics/api/engine_report/product_stats.php +++ b/app/ics/api/engine_report/product_stats.php @@ -2,35 +2,11 @@ session_start(); require '../../../assets/utils/db_auth.php'; + require '../../../assets/utils/classes/Reports.php'; + $report = new Reports($pdo2, $company_id); - $sql = "SELECT SUM(total_in) as total_in, SUM(total_out) as total_out, b.min_stock - FROM warehouse_balance a - LEFT JOIN md_product b ON - a.company_id = b.company_id AND - a.product_sku = b.sku - WHERE - a.company_id = :company_id - GROUP BY a.product_sku - "; - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $output = $sth->fetch(PDO::FETCH_ASSOC); - - // calculate stock balance - $low_stock = 0; - foreach($output as $key => $value) { - $output[$key]["low_stock"] = ($value["total_in"] - $value["total_out"]) < $value["min_stock"]?true:false; - if ($output[$key]["low_stock"]) { - $low_stock++; - } - } - $answer["output"] = $output; - $answer["low_stock"] = $low_stock; + $answer["low_stock"] = $report->getLowStockCount(); $answer["success"] = 1; exit(json_encode($answer)); diff --git a/app/ics/api/engine_report/stock_activity.php b/app/ics/api/engine_report/stock_activity.php index a34bae6..d8bb3af 100644 --- a/app/ics/api/engine_report/stock_activity.php +++ b/app/ics/api/engine_report/stock_activity.php @@ -2,6 +2,7 @@ session_start(); require '../../../assets/utils/db_auth.php'; + require '../../../assets/utils/classes/WarehouseManager.php'; $id = 0; $warehouse_id = (int)$data['warehouse']; diff --git a/app/ics/api/engine_report/stock_movement.php b/app/ics/api/engine_report/stock_movement.php index 82a71c3..20b12a9 100644 --- a/app/ics/api/engine_report/stock_movement.php +++ b/app/ics/api/engine_report/stock_movement.php @@ -2,6 +2,7 @@ session_start(); require '../../../assets/utils/db_auth.php'; + require '../../../assets/utils/classes/WarehouseManager.php'; $id = 0; $warehouse_id = (int)$data['warehouse']; diff --git a/app/ics/api/engine_report/stock_trend.php b/app/ics/api/engine_report/stock_trend.php index aa6af5c..2571776 100644 --- a/app/ics/api/engine_report/stock_trend.php +++ b/app/ics/api/engine_report/stock_trend.php @@ -2,6 +2,7 @@ session_start(); require '../../../assets/utils/db_auth.php'; + require '../../../assets/utils/classes/WarehouseManager.php'; $id = 0; $warehouse_id = (int)$data['warehouse']; diff --git a/app/ics/api/engine_report/warehouse_stats.php b/app/ics/api/engine_report/warehouse_stats.php index 6ff71b1..065e892 100644 --- a/app/ics/api/engine_report/warehouse_stats.php +++ b/app/ics/api/engine_report/warehouse_stats.php @@ -2,22 +2,12 @@ session_start(); require '../../../assets/utils/db_auth.php'; + require '../../../assets/utils/classes/Reports.php'; + $report = new Reports($pdo2, $company_id); - $sql = "SELECT SUM(capacity) as total_capacity, count(*) as total_warehouse - FROM md_warehouse a - WHERE - a.company_id = :company_id - "; - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $output = $sth->fetch(PDO::FETCH_ASSOC); - - $answer["output"] = $output; + $answer["total_capacity"] = $report->getTotalCapacity(); + $answer["total_warehouse"] = $report->getTotalWarehouse(); $answer["success"] = 1; exit(json_encode($answer)); diff --git a/app/ics/expired-stock.php b/app/ics/expired-stock.php new file mode 100644 index 0000000..94d5b6b --- /dev/null +++ b/app/ics/expired-stock.php @@ -0,0 +1,351 @@ + + + + + + + + +
+
+
+
+
+

Expired / Near Expiry Products +

+

Monitor and manage products approaching or past expiry dates

+ +
+
+
+
+
+
+
+
+ + + + +
+
+ 3 + Expired Products + + + Requires immediate action +
+ +
+ +
+
+
+
+
+
+ + + + +
+
+ 2 + Critical (≤7 days) + + + Expiring within a week +
+ +
+ +
+ + +
+
+
+
+
+ + + +
+
+ 2 + Warning (8-14 days) + + + Expiring in 2 weeks +
+ +
+ +
+ +
+
+
+
+
+ + + +
+
+ 2 + Caution (15-30 days) + + + Monitor closely +
+ +
+ +
+ +
+ +
+
+
+
+
+

Filter Products

+
+
+
+ + + + + +
+
+
+
+ + + +
+ +
+ + +
+ + +
+ +
+ + +
+ +
+
+
+
+
+
+

Expired Products

+

Products that have passed their expiry date and need to be removed

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Product NameSKUCategoryExpiry DateDays OverdueQuantityStatusAction
Wireless MouseWM-001Electronics2025-11-1547 days12Expired + +
USB-C CableUC-002Accessories2025-12-0131 days8Expired + +
KeyboardKB-004Electronics2025-12-2012 days5Expired + +
+ +
+
+
+
+
+
+
+
+
+
+

Near Expiry Products

+

Products approaching their expiry date that may need attention

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Product NameSKUCategoryExpiry DateDays RemainingQuantityStatusAction
Laptop StandLS-003Furniture2026-01-054 days15Critical (≤7 days) + Mark For Sale +
Monitor MN-005Electronics2026-01-087 days10Critical (≤7 days) + Mark For Sale +
Desk LampDL-006Furniture2026-01-1211 days20Warning (8-14 days) + Mark For Sale +
Webcam HDWC-007Electronics2026-01-1817 days18Caution (15-30 days) + Mark For Sale +
HDMI CableHD-008Accessories2026-01-2524 days30Caution (15-30 days) + Mark For Sale +
+ +
+
+
+
+
+ +
+
+
+

Copyright © 2026 InApp Inventory Dashboard. Developed by CodesCandy

+
+
+ +
+ + +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/app/ics/low-stock-products.php b/app/ics/low-stock-products.php new file mode 100644 index 0000000..6f3699c --- /dev/null +++ b/app/ics/low-stock-products.php @@ -0,0 +1,203 @@ + + + + + + + + +
+
+
+
+
+

Low Stock Products +

+ + + Restock All + +
+
+
+
+
+
+
+
+ Total Low Stock + + + +
+
+ 6 + + Products need attention +
+ +
+ +
+ +
+
+
+
+
+ Critical Level + + + +
+
+ 3 + + Immediate restock needed +
+ +
+ +
+ +
+
+
+
+
+ Warning Level + + + +
+
+ 3 + + Restock soon +
+ +
+ +
+ +
+ +
+
+
+
+
+

Low Stock Items

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProductSKUCurrent StockMin StockWarehouseStatusAction
Product ASKU-001520Main WarehouseCriticalRestock
Product BSKU-0021225Main WarehouseWarningRestock
Product CSKU-003315Secondary WarehouseCriticalRestock
Product DSKU-0041830Main WarehouseWarningRestock
Product ESKU-005210Secondary WarehouseCriticalRestock
Product FSKU-006820Main WarehouseWarningRestock
+
+
+ +
+ +
+ +
+ +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/app/ics/stock_overview.php b/app/ics/stock_overview.php index ab307af..e82bf24 100644 --- a/app/ics/stock_overview.php +++ b/app/ics/stock_overview.php @@ -198,8 +198,8 @@ action: 'read', onSuccess: function(res) { - $("#capacity h3").text(format_number(res.output.total_capacity, 2)); - $("#capacity p").text("Accross " + format_number(res.output.total_warehouse) + " warehouses"); + $("#capacity h3").text(format_number(res.total_capacity, 2)); + $("#capacity p").text("Accross " + format_number(res.total_warehouse) + " warehouses"); $("#capacity .card-loader-overlay").hide(); } @@ -219,8 +219,6 @@ action: 'read', onSuccess: function(res) { - console.log(res); - $("#total_stock_in h3").text(format_number(res.output.total_in, 2)); $("#total_stock_in p").text("Accross " + format_number(res.output.total_warehouse) + " warehouses"); $("#total_stock_in .card-loader-overlay").hide(); diff --git a/app/include_sidebar.php b/app/include_sidebar.php index 12fb427..a943008 100644 --- a/app/include_sidebar.php +++ b/app/include_sidebar.php @@ -20,35 +20,14 @@
  • - + Dashboard
  • - - - Inventory - -
  • - -
  • - - - Low Stock Products - -
  • - -
  • - - - Expired Stock - -
  • - -
  • - + Reports @@ -113,6 +92,20 @@
  • +
  • + + + Low Stock Products + +
  • + +
  • + + + Expired Stock + +
  • + diff --git a/app/inventory/api/engine/manage_product.php b/app/inventory/api/engine/manage_product.php index 1214aad..e968e37 100644 --- a/app/inventory/api/engine/manage_product.php +++ b/app/inventory/api/engine/manage_product.php @@ -2,6 +2,7 @@ session_start(); require '../../../assets/utils/db_auth.php'; + require '../../../assets/utils/classes/FileUploader.php'; $id = (int)$data['id']; @@ -57,6 +58,7 @@ sku = :sku, price = :price, min_stock = :min_stock, + reorder_point = :reorder_point, category = :category, product_image = :product_image, `description` = :description, @@ -72,6 +74,7 @@ ":sku" => $data["sku"], ":price" => $data["price"], ":min_stock" => $data["min_stock"], + ":reorder_point" => $data["reorder_point"], ":category" => $data["category"], ":product_image"=> $db_image_string, ":description" => $data["description"], @@ -82,9 +85,9 @@ } else { $sql = "INSERT INTO md_product - (company_id, product_name, sku, price, min_stock, category, product_image, `description`, `status`, `log`) + (company_id, product_name, sku, price, min_stock, reorder_point, category, product_image, `description`, `status`, `log`) VALUES - (:company_id, :product_name, :sku, :price, :min_stock, :category, :product_image, :description, :status, :log)"; + (:company_id, :product_name, :sku, :price, :min_stock, :reorder_point, :category, :product_image, :description, :status, :log)"; $sth = $pdo->prepare($sql); $sth->execute([ @@ -93,6 +96,7 @@ ":sku" => $data["sku"], ":price" => $data["price"], ":min_stock" => $data["min_stock"], + ":reorder_point" => $data["reorder_point"], ":category" => $data["category"], ":product_image"=> $db_image_string, ":description" => $data["description"], diff --git a/app/inventory/api/engine/product.php b/app/inventory/api/engine/product.php index 9094c59..547b7c4 100644 --- a/app/inventory/api/engine/product.php +++ b/app/inventory/api/engine/product.php @@ -3,10 +3,14 @@ require '../../../assets/utils/db_auth.php'; - $sql = "SELECT * - FROM md_product + $sql = "SELECT a.* , IFNULL(SUM(b.total_in-b.total_out),0) AS product_balance + FROM md_product a + LEFT JOIN warehouse_balance b ON + a.company_id = b.company_id AND + a.sku = b.product_sku WHERE - company_id = {$company_id}"; + a.company_id = {$company_id} + GROUP BY a.id"; $sth = $pdo2->prepare($sql); $sth->execute([ ":company_id" => $company_id diff --git a/app/inventory/api/engine/product_category.php b/app/inventory/api/engine/product_category.php index 76ec232..7e90c0e 100644 --- a/app/inventory/api/engine/product_category.php +++ b/app/inventory/api/engine/product_category.php @@ -3,10 +3,14 @@ require '../../../assets/utils/db_auth.php'; - $sql = "SELECT * - FROM md_product_category + $sql = "SELECT a.* , COUNT(b.id) AS product_count + FROM md_product_category a + LEFT JOIN md_product b ON + a.company_id = b.company_id AND + a.id = b.category WHERE - company_id = {$company_id}"; + a.company_id = {$company_id} + GROUP BY a.id"; $sth = $pdo2->prepare($sql); $sth->execute([ ":company_id" => $company_id diff --git a/app/inventory/api/engine/product_stats.php b/app/inventory/api/engine/product_stats.php new file mode 100644 index 0000000..86ba61e --- /dev/null +++ b/app/inventory/api/engine/product_stats.php @@ -0,0 +1,16 @@ +getTotalCategory(); + $answer["active_category"] = $report->getActiveCategory(); + $answer["inactive_category"] = $report->getInactiveCategory(); + $answer["total_product"] = $report->getTotalProduct(); + $answer["success"] = 1; + exit(json_encode($answer)); + +?> \ No newline at end of file diff --git a/app/inventory/api/engine/warehouse.php b/app/inventory/api/engine/warehouse.php index d3c15aa..ad30552 100644 --- a/app/inventory/api/engine/warehouse.php +++ b/app/inventory/api/engine/warehouse.php @@ -3,10 +3,17 @@ require '../../../assets/utils/db_auth.php'; - $sql = "SELECT a.* + $sql = "SELECT a.*, IFNULL(COUNT(DISTINCT b.product_sku), 0) AS unique_product, + c.name AS manager_name, c.surname AS manager_surname FROM md_warehouse a + LEFT JOIN warehouse_balance b ON + a.company_id = b.company_id AND + a.id = b.warehouse_id + LEFT JOIN wms.user c ON + a.manager = c.user_id WHERE - a.company_id = :company_id"; + a.company_id = :company_id + GROUP BY a.id"; $sth = $pdo2->prepare($sql); $sth->execute([ ":company_id" => $company_id @@ -14,20 +21,6 @@ db_check($sth, $answer); $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - - // modify output - foreach($answer["output"] as $key => $item){ - // status - if($item["status"]==1){ - $answer["output"][$key]["status"] = 'Active'; - $answer["output"][$key]["bg_class"] = 'success'; - }else{ - $answer["output"][$key]["status"] = 'Inactive'; - $answer["output"][$key]["bg_class"] = 'secondary'; - - } - } - $answer["success"] = 1; exit(json_encode($answer)); diff --git a/app/inventory/api/engine/warehouse_stats.php b/app/inventory/api/engine/warehouse_stats.php new file mode 100644 index 0000000..e6c0ae4 --- /dev/null +++ b/app/inventory/api/engine/warehouse_stats.php @@ -0,0 +1,17 @@ +getTotalWarehouse(); + $answer["total_location"] = $report->getTotalLocation(); + $answer["total_capacity"] = $report->getTotalCapacity(); + $answer["total_product"] = $report->getTotalProduct(); + $answer["success"] = 1; + + exit(json_encode($answer)); + +?> \ No newline at end of file diff --git a/app/inventory/manage_product.php b/app/inventory/manage_product.php index f07d6c1..d290062 100644 --- a/app/inventory/manage_product.php +++ b/app/inventory/manage_product.php @@ -43,17 +43,28 @@ -
    - +
    +
    +
    + + +
    -
    +
    +
    + + +
    @@ -109,7 +120,7 @@ url: "inventory/api/engine/manage_product.php", autoPrepare: true, checkRequired: 0, - debugMode: 1, + debugMode: 0, formData: formData, action: 'manage', onSuccess: function(res) { diff --git a/app/inventory/manage_warehouse.php b/app/inventory/manage_warehouse.php index 379bc6f..4d2732a 100644 --- a/app/inventory/manage_warehouse.php +++ b/app/inventory/manage_warehouse.php @@ -40,10 +40,17 @@
    -
    +
    +
    + + +
    diff --git a/app/inventory/product.php b/app/inventory/product.php index 3c6d189..9846161 100644 --- a/app/inventory/product.php +++ b/app/inventory/product.php @@ -33,7 +33,7 @@
    Total Categories -

    6

    +

    6

    @@ -52,7 +52,7 @@
    Active -

    5

    +

    5

    @@ -71,7 +71,7 @@
    Inactive -

    1

    +

    1

    @@ -90,7 +90,7 @@
    Total Products -

    356

    +

    356

    @@ -194,6 +194,7 @@ SKU Current Stock Min Stock + Reorder Point Status Action @@ -217,6 +218,29 @@