more OOP and restructure utils

This commit is contained in:
Thanakorn S
2026-04-22 15:55:18 +07:00
parent 8834d672ba
commit 35dd1a9fce
31 changed files with 1417 additions and 494 deletions
BIN
View File
Binary file not shown.
+162
View File
@@ -0,0 +1,162 @@
<?php
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})";
}
}
?>
+121
View File
@@ -0,0 +1,121 @@
<?php
class Reports
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $companyId)
{
$this->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);
}
}
?>
@@ -0,0 +1,105 @@
<?php
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
]);
}
}
?>
+1 -368
View File
@@ -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;
}
}
}
?>
+100
View File
@@ -0,0 +1,100 @@
<?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.";
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;
}
}
}
?>
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/FileUploader.php';
$id = (int)$data['id'];
+2 -14
View File
@@ -482,23 +482,11 @@
</div>
</div>
<div class="row">
<div class="col-12">
<footer class="text-center py-2 mt-6 text-secondary ">
<p class="mb-0">Copyright © 2026 InApp Inventory Dashboard. Developed by <a href="https://codescandy.com/"
target="_blank" class="text-primary">CodesCandy</a> </p>
</footer>
</div>
</div>
<div class="position-fixed end-0 bottom-0 m-4">
<a href="https://codescandy.com/template/inventory-admin-dashboard-template/" class="btn btn-dark"
target="_blank"> Download Now</a>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<!-- Bootstrap JS -->
+186
View File
@@ -0,0 +1,186 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-5">
<div class="">
<h1 class="fs-3 mb-1">Reports</h1>
<p class="mb-0">View your inventory analytics and reports</p>
</div>
</div>
</div>
</div>
<div class="row g-5 mb-5">
<!-- Stat cards -->
<div class="col-12 col-sm-6 col-md-3">
<div class="card h-100">
<div class="card-body p-6">
<h6 class="mb-5 ">Total Revenue</h6>
<h3 class="mb-1 fw-bold">$45,231</h3>
<p class="mb-0 text-success small"><i class="ti ti-arrow-up"> </i>12% from last month</p>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="card h-100">
<div class="card-body p-6">
<h6 class="mb-5 ">Products Sold</h6>
<h3 class="mb-1 fw-bold">1,234</h3>
<p class="mb-0 text-success small"><i class="ti ti-arrow-up"> </i> 8% from last month</p>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="card h-100">
<div class="card-body p-6">
<h6 class="mb-5 ">Low Stock Items</h6>
<h3 class="mb-1 fw-bold">23</h3>
<p class="mb-0 text-danger small"><i class="ti ti-arrow-down"> </i> 3% from last month</p>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="card h-100">
<div class="card-body p-6">
<h6 class="mb-5 ">Out of Stock</h6>
<h3 class="mb-1 fw-bold">5</h3>
<p class="mb-0 text-danger small"><i class="ti ti-arrow-down"> </i> 2% from last month</p>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<!-- Sales Overview (full width) -->
<div class="col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start mb-3 gap-2">
<div>
<h2 class="mb-0 fs-5">Sales Overview</h2>
</div>
<div class="controls">
<button id="btn-random" class="btn btn-light btn-sm">Randomize Data</button>
<button id="btn-update" class="btn btn-primary btn-sm">Show This Year Only</button>
</div>
</div>
<!-- Chart placeholder: replace with canvas or chart container when integrating chart library -->
<div id="salesChart"></div>
<div class="d-flex justify-content-end">
<a href="#" class="small">View detailed report</a>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Top Products -->
<div class="col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h2 class="mb-0 fs-5">Top Products</h2>
</div>
</div>
<!-- Product rows -->
<div class="list-group list-group-flush">
<div class="list-group-item p-3 d-flex align-items-center">
<div class="me-3">
<img src="./assets/images/product-1.png" alt="Product A" class="rounded"
style="width:48px; height:48px; object-fit:cover;">
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">Gaming Joy Stick</h6>
<small class="text-secondary">156 units sold</small>
</div>
<div class="text-end">
<strong>$3,120</strong>
</div>
</div>
</div>
</div>
<div class="list-group-item p-3 d-flex align-items-center">
<div class="me-3">
<img src="./assets/images/product-2.png" alt="Product B" class="rounded"
style="width:48px; height:48px; object-fit:cover;">
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">Wireless Headphones</h6>
<small class="text-secondary">134 units sold</small>
</div>
<div class="text-end">
<strong>$2,680</strong>
</div>
</div>
</div>
</div>
<div class="list-group-item p-3 d-flex align-items-center">
<div class="me-3">
<img src="./assets/images/product-3.png" alt="Product C" class="rounded"
style="width:48px; height:48px; object-fit:cover;">
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">Smartwatch</h6>
<small class="text-secondary">98 units sold</small>
</div>
<div class="text-end">
<strong>$1,960</strong>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<!-- Bootstrap JS -->
</body>
</html>
+1
View File
@@ -2,6 +2,7 @@
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
try {
+1
View File
@@ -2,6 +2,7 @@
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
try {
@@ -2,6 +2,7 @@
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
try {
+3 -27
View File
@@ -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));
@@ -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'];
@@ -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'];
@@ -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'];
+4 -14
View File
@@ -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));
+351
View File
@@ -0,0 +1,351 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row ">
<div class="col-12">
<div class="mb-6 ">
<h1 class="fs-3 mb-1">Expired / Near Expiry Products
</h1>
<p class="mb-0">Monitor and manage products approaching or past expiry dates</p>
</div>
</div>
</div>
<div class="row mb-5 g-5">
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body p-5">
<div class="">
<span class="icon-shape icon-md bg-primary text-white rounded-2">
<i class="ti ti-trash fs-4"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block">3</span>
<span class="fs-5">Expired Products</span>
<small class="text-muted">
Requires immediate action</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body p-5">
<div class="">
<span class="icon-shape icon-md bg-primary text-white rounded-2">
<i class="ti ti-alert-triangle fs-4"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block">2</span>
<span class="fs-5">Critical (≤7 days)</span>
<small class="text-muted">
Expiring within a week</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body p-5">
<div class="">
<span class="icon-shape icon-md bg-primary text-white rounded-2">
<i class="ti ti-clock fs-4"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block">2</span>
<span class="fs-5">Warning (8-14 days)</span>
<small class="text-muted">
Expiring in 2 weeks</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body p-5">
<div class="">
<span class="icon-shape icon-md bg-primary text-white rounded-2">
<i class="ti ti-calendar-clock fs-4"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block">2</span>
<span class="fs-5">Caution (15-30 days)</span>
<small class="text-muted">
Monitor closely</small>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5 mb-5">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<h2 class="fs-4 mb-4">Filter Products</h2>
<div class="row g-5">
<div class="col-lg-7">
<div class="position-relative">
<input type="search" class="form-control ps-9" placeholder="Search by product name or SKU">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<div class="col-lg-5">
<div class="d-flex gap-3">
<select class="form-select">
<option selected>All Status</option>
<option value="Expired">Expired</option>
<option value="Critical">Critical</option>
<option value="Warning">Warning</option>
<option value="Caution">Caution</option>
</select>
<select class="form-select">
<option selected>All Categories</option>
<option value="Category 1">Category 1</option>
<option value="Category 2">Category 2</option>
<option value="Category 3">Category 3</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5 mb-5">
<div class="col-12">
<div class="card ">
<div class="card-body p-5">
<div>
<h2 class="fs-4 mb-2">Expired Products</h2>
<p>Products that have passed their expiry date and need to be removed</p>
</div>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered">
<thead class="table-light">
<tr>
<th class="py-3">Product Name</th>
<th class="py-3">SKU</th>
<th class="py-3">Category</th>
<th class="py-3">Expiry Date</th>
<th class="py-3">Days Overdue</th>
<th class="py-3">Quantity</th>
<th class="py-3">Status</th>
<th class="py-3">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="py-3">Wireless Mouse</td>
<td class="py-3">WM-001</td>
<td class="py-3">Electronics</td>
<td class="py-3">2025-11-15</td>
<td class="py-3">47 days</td>
<td class="py-3">12</td>
<td class="py-3"><span class="badge bg-danger">Expired</span></td>
<td class="py-3">
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>
<tr>
<td class="py-3">USB-C Cable</td>
<td class="py-3">UC-002</td>
<td class="py-3">Accessories</td>
<td class="py-3">2025-12-01</td>
<td class="py-3">31 days</td>
<td class="py-3">8</td>
<td class="py-3"><span class="badge bg-danger">Expired</span></td>
<td class="py-3">
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>
<tr>
<td class="py-3">Keyboard</td>
<td class="py-3">KB-004</td>
<td class="py-3">Electronics</td>
<td class="py-3">2025-12-20</td>
<td class="py-3">12 days</td>
<td class="py-3">5</td>
<td class="py-3"><span class="badge bg-danger">Expired</span></td>
<td class="py-3">
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-12">
<div class="card ">
<div class="card-body p-5">
<div>
<h2 class="fs-4 mb-2">Near Expiry Products</h2>
<p>Products approaching their expiry date that may need attention</p>
</div>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered">
<thead class="table-light">
<tr>
<th class="py-3">Product Name</th>
<th class="py-3">SKU</th>
<th class="py-3">Category</th>
<th class="py-3">Expiry Date</th>
<th class="py-3">Days Remaining</th>
<th class="py-3">Quantity</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="py-3">Laptop Stand</td>
<td class="py-3">LS-003</td>
<td class="py-3">Furniture</td>
<td class="py-3">2026-01-05</td>
<td class="py-3 text-primary">4 days</td>
<td class="py-3">15</td>
<td class="py-3"><span class="badge bg-primary">Critical (≤7 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
<tr>
<td class="py-3">Monitor</td>
<td class="py-3"> MN-005</td>
<td class="py-3">Electronics</td>
<td class="py-3">2026-01-08</td>
<td class="py-3">7 days</td>
<td class="py-3">10</td>
<td class="py-3"><span class="badge bg-primary">Critical (≤7 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
<tr>
<td class="py-3">Desk Lamp</td>
<td class="py-3">DL-006</td>
<td class="py-3">Furniture</td>
<td class="py-3">2026-01-12</td>
<td class="py-3">11 days</td>
<td class="py-3">20</td>
<td class="py-3"><span class="badge bg-warning">Warning (8-14 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
<tr>
<td class="py-3">Webcam HD</td>
<td class="py-3">WC-007</td>
<td class="py-3">Electronics</td>
<td class="py-3">2026-01-18</td>
<td class="py-3">17 days</td>
<td class="py-3">18</td>
<td class="py-3"><span class="badge bg-warning">Caution (15-30 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
<tr>
<td class="py-3">HDMI Cable</td>
<td class="py-3">HD-008</td>
<td class="py-3">Accessories</td>
<td class="py-3">2026-01-25</td>
<td class="py-3">24 days</td>
<td class="py-3">30</td>
<td class="py-3"><span class="badge bg-warning">Caution (15-30 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<footer class="text-center py-2 mt-6 text-secondary ">
<p class="mb-0">Copyright © 2026 InApp Inventory Dashboard. Developed by <a href="https://codescandy.com/"
target="_blank" class="text-primary">CodesCandy</a> </p>
</footer>
</div>
</div>
<div class="position-fixed end-0 bottom-0 m-4">
<a href="https://codescandy.com/template/inventory-admin-dashboard-template/" class="btn btn-dark"
target="_blank"> Download Now</a>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<!-- Bootstrap JS -->
</body>
</html>
+203
View File
@@ -0,0 +1,203 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row ">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<h1 class="fs-3 mb-0">Low Stock Products
</h1>
<a href="#!" class="btn btn-primary">
<span><i class="ti ti-plus"></i></span>
<span>Restock All</span></a>
</div>
</div>
</div>
<div class="row mb-5 g-5">
<div class="col-lg-4 col-12">
<div class="card border border-warning border-opacity-25">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<span>Total Low Stock</span>
<span>
<i class="ti ti-alert-triangle fs-3 text-warning"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-3 fw-bold mb-0 d-block text-warning">6</span>
<small class="text-muted">
Products need attention</small>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-12">
<div class="card border border-danger border-opacity-25">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<span>Critical Level</span>
<span>
<i class="ti ti-activity-heartbeat fs-3 text-danger"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-3 fw-bold mb-0 d-block text-danger">3</span>
<small class="text-muted">
Immediate restock needed</small>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-12">
<div class="card border border-success border-opacity-25">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<span>Warning Level</span>
<span>
<i class="ti ti-box-seam fs-3 text-success"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-3 fw-bold mb-0 d-block text-success">3</span>
<small class="text-muted">
Restock soon</small>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<h2 class="mb-4 fs-5">Low Stock Items</h2>
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered">
<thead class="table-light">
<tr>
<th>Product</th>
<th>SKU</th>
<th>Current Stock</th>
<th>Min Stock</th>
<th>Warehouse</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="py-3">Product A</td>
<td class="py-3 text-secondary">SKU-001</td>
<td class="py-3 text-danger">5</td>
<td class="py-3">20</td>
<td class="py-3">Main Warehouse</td>
<td class="py-3"><span class="badge bg-danger rounded-pill">Critical</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product B</td>
<td class="py-3 text-secondary">SKU-002</td>
<td class="py-3 text-warning">12</td>
<td class="py-3">25</td>
<td class="py-3">Main Warehouse</td>
<td class="py-3"><span
class="badge bg-secondary bg-opacity-10 text-dark rounded-pill">Warning</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product C</td>
<td class="py-3 text-secondary">SKU-003</td>
<td class="py-3 text-danger">3</td>
<td class="py-3">15</td>
<td class="py-3">Secondary Warehouse</td>
<td class="py-3"><span class="badge bg-danger rounded-pill">Critical</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product D</td>
<td class="py-3 text-secondary">SKU-004</td>
<td class="py-3 text-warning">18</td>
<td class="py-3">30</td>
<td class="py-3">Main Warehouse</td>
<td class="py-3"><span
class="badge bg-secondary bg-opacity-10 text-dark rounded-pill">Warning</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product E</td>
<td class="py-3 text-secondary">SKU-005</td>
<td class="py-3 text-danger">2</td>
<td class="py-3">10</td>
<td class="py-3">Secondary Warehouse</td>
<td class="py-3"><span class="badge bg-danger rounded-pill">Critical</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product F</td>
<td class="py-3 text-secondary">SKU-006</td>
<td class="py-3 text-warning">8</td>
<td class="py-3">20</td>
<td class="py-3">Main Warehouse</td>
<td class="py-3"><span
class="badge bg-secondary bg-opacity-10 text-dark rounded-pill">Warning</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<!-- Table rows will be dynamically populated here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<!-- Bootstrap JS -->
</body>
</html>
+2 -4
View File
@@ -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();
+16 -23
View File
@@ -20,35 +20,14 @@
</li>
<li>
<a class="nav-link active" href="index.html">
<a class="nav-link active" href="<?php echo $server_url?>dashboard/index.php">
<i class="ti ti-home"></i>
<span class="nav-text">Dashboard</span>
</a>
</li>
<li>
<a class="nav-link" href="inventory.html">
<i class="ti ti-box-seam"></i>
<span class="nav-text">Inventory</span>
</a>
</li>
<li>
<a class="nav-link" href="low-stock-products.html">
<i class="ti ti-alert-triangle"></i>
<span class="nav-text">Low Stock Products</span>
</a>
</li>
<li>
<a class="nav-link" href="stock-expired.html">
<i class="ti ti-clock"></i>
<span class="nav-text">Expired Stock</span>
</a>
</li>
<li>
<a class="nav-link" href="reports.html">
<a class="nav-link" href="<?php echo $server_url?>dashboard/reports.php">
<i class="ti ti-receipt"></i>
<span class="nav-text">Reports</span>
</a>
@@ -113,6 +92,20 @@
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/low-stock-products.php">
<i class="ti ti-alert-triangle"></i>
<span class="nav-text">Low Stock Products</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/expired-stock.php">
<i class="ti ti-clock"></i>
<span class="nav-text">Expired Stock</span>
</a>
</li>
+6 -2
View File
@@ -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"],
+7 -3
View File
@@ -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
@@ -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
@@ -0,0 +1,16 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/Reports.php';
$report = new Reports($pdo2, $company_id);
$answer["total_category"] = $report->getTotalCategory();
$answer["active_category"] = $report->getActiveCategory();
$answer["inactive_category"] = $report->getInactiveCategory();
$answer["total_product"] = $report->getTotalProduct();
$answer["success"] = 1;
exit(json_encode($answer));
?>
+9 -16
View File
@@ -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));
@@ -0,0 +1,17 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/Reports.php';
$report = new Reports($pdo2, $company_id);
$answer["total_warehouse"] = $report->getTotalWarehouse();
$answer["total_location"] = $report->getTotalLocation();
$answer["total_capacity"] = $report->getTotalCapacity();
$answer["total_product"] = $report->getTotalProduct();
$answer["success"] = 1;
exit(json_encode($answer));
?>
+15 -4
View File
@@ -43,17 +43,28 @@
<label for="price" class="form-label">Price</label>
<input type="number" class="form-control" id="price" placeholder="0.00" step="0.01" required>
</div>
<div class="col-md-6 mb-3">
<label for="min_stock" class="form-label">min_stock</label>
<div class="col-md-3 mb-3">
<label for="min_stock" class="form-label">Min Stock</label>
<input type="number" class="form-control" id="min_stock" placeholder="0" required>
</div>
<div class="col-md-3 mb-3">
<label for="reorder_point" class="form-label">Reorder Point</label>
<input type="number" class="form-control" id="reorder_point" placeholder="0" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<div class="col-md-3 mb-3">
<label for="category" class="form-label">Category</label>
<select class="form-select" id="category" required> </select>
</div>
<div class="col-md-3 mb-3">
<label for="status" class="form-label">status</label>
<select class="form-select" id="status" required>
<option value="1">Active</option>
<option value="0">inactive</option>
</select>
</div>
<div class="col-md-6 mb-3">
<!-- Remove d-none from file input so it shows by default -->
<div class="mb-3">
@@ -109,7 +120,7 @@
url: "<?php echo $server_url?>inventory/api/engine/manage_product.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 1,
debugMode: 0,
formData: formData,
action: 'manage',
onSuccess: function(res) {
+8 -1
View File
@@ -40,10 +40,17 @@
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<div class="col-md-3 mb-3">
<label for="manager" class="form-label">Manager</label>
<select class="form-select" id="manager" required> </select>
</div>
<div class="col-md-3 mb-3">
<label for="status" class="form-label">status</label>
<select class="form-select" id="status" required>
<option value="1">Active</option>
<option value="0">inactive</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="capacity" class="form-label">Capacity</label>
<input type="number" class="form-control" id="capacity" placeholder="0.00" step="0.01" required>
+37 -8
View File
@@ -33,7 +33,7 @@
</div>
<div class="">
<span>Total Categories</span>
<h2 class="fw-bold mb-0">6</h2>
<h2 class="fw-bold mb-0" id="total_category">6</h2>
</div>
</div>
@@ -52,7 +52,7 @@
</div>
<div class="">
<span>Active</span>
<h2 class="fw-bold mb-0">5</h2>
<h2 class="fw-bold mb-0" id="active_category">5</h2>
</div>
</div>
@@ -71,7 +71,7 @@
</div>
<div class="">
<span>Inactive</span>
<h2 class="fw-bold mb-0">1</h2>
<h2 class="fw-bold mb-0" id="inactive_category">1</h2>
</div>
</div>
@@ -90,7 +90,7 @@
</div>
<div class="">
<span>Total Products</span>
<h2 class="fw-bold mb-0">356</h2>
<h2 class="fw-bold mb-0" id="total_product">356</h2>
</div>
</div>
@@ -194,6 +194,7 @@
<th>SKU</th>
<th>Current Stock</th>
<th>Min Stock</th>
<th>Reorder Point</th>
<th>Status</th>
<th>Action</th>
</tr>
@@ -217,6 +218,29 @@
<?php require "../include_ending.php";?>
<script>
function retrieve_stats() {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/product_stats.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
debugMode: 0,
onSuccess: function(res) {
$("#total_category").text(res.total_category);
$("#active_category").text(res.active_category);
$("#inactive_category").text(res.inactive_category);
$("#total_product").text(res.total_product);
}
});
}
function retrieve_category() {
return ajax_request({
@@ -224,6 +248,7 @@
autoPrepare: true,
checkRequired: 0,
action: 'read',
debugMode: 0,
onSuccess: function(res) {
// Register the array as a table name
@@ -271,6 +296,7 @@
}
function change_page_category(page_num) {
var offset = (page_num - 1) * prop_limit;
@@ -283,9 +309,9 @@
<td class="py-3">${item['category']}</td>
<td class="py-3">${item['slug']}</td>
<td class="py-3">${item['description']}</td>
<td class="py-3">${item['status']}</td>
<td class="py-3">${item['product_count']}</td>
<td class="py-3">
${(item['status'])?'<span class="badge bg-success">Active</span>':'<span class="badge bg-secondary">Inactive</span>'}
${(item['status']==1)?'<span class="badge bg-success">Active</span>':'<span class="badge bg-secondary">Inactive</span>'}
</td>
<td class="py-3">
<a href="<?php echo $server_url?>inventory/manage_category.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
@@ -332,6 +358,7 @@
}
function change_page_product(page_num) {
var offset = (page_num - 1) * prop_limit;
@@ -343,10 +370,11 @@
body += `<tr>
<td class="py-3">${item['product_name']}</td>
<td class="py-3">${item['sku']}</td>
<td class="py-3">${item['description']}</td>
<td class="py-3" style='text-align: right;'>${format_number(item['product_balance'],4)}</td>
<td class="py-3" style='text-align: right;'>${format_number(item['min_stock'],2)}</td>
<td class="py-3" style='text-align: right;'>${format_number(item['reorder_point'],2)}</td>
<td class="py-3">
${(item['status'])?'<span class="badge bg-success">Active</span>':'<span class="badge bg-secondary">Inactive</span>'}
${(item['status']==1)?'<span class="badge bg-success">Active</span>':'<span class="badge bg-secondary">Inactive</span>'}
</td>
<td class="py-3">
<a href="<?php echo $server_url?>inventory/manage_product.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
@@ -361,6 +389,7 @@
$(async function() {
try {
await retrieve_stats();
await retrieve_category();
await retrieve_product();
} catch (e) {
+32 -7
View File
@@ -29,7 +29,7 @@
<div class="card ">
<div class="card-body p-0">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap ">
<div class=" p-6 flex-grow-1 d-flex justify-content-between">
<div class=" p-6 flex-grow-1 d-flex justify-content-between" id="total_warehouse">
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">5</span>
<span class="fs-5">Total Warehouses</span>
@@ -50,7 +50,7 @@
</div>
</div>
<div class=" p-6 flex-grow-1 d-flex justify-content-between">
<div class=" p-6 flex-grow-1 d-flex justify-content-between" id="total_capacity">
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">41,000</span>
<span class="fs-5">Total Capacity</span>
@@ -78,7 +78,7 @@
</div>
</div>
<div class=" p-6 flex-grow-1 d-flex justify-content-between">
<div class=" p-6 flex-grow-1 d-flex justify-content-between" id="total_space_used">
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">25,500</span>
<span class="fs-5">Space Used</span>
@@ -101,7 +101,7 @@
</div>
</div>
<div class="-0 p-6 flex-grow-1 d-flex justify-content-between">
<div class="-0 p-6 flex-grow-1 d-flex justify-content-between" id="total_product">
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">730</span>
<span class="fs-5">Total Products</span>
@@ -251,6 +251,30 @@
<?php require "../include_ending.php";?>
<script>
function retrieve_stats() {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/warehouse_stats.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
$("#total_warehouse span.fs-2").text(res.total_warehouse);
$("#total_warehouse small.text-info").text(`Across ${res.total_location} locations`);
$("#total_capacity span.fs-2").text(format_number(res.total_capacity,2));
$("#total_product span.fs-2").text(res.total_product);
}
});
}
function retrieve_warehouse() {
return ajax_request({
@@ -294,11 +318,11 @@
body += `<tr>
<td class="py-3">${item['warehouse_name']}</td>
<td class="py-3">${item['location']}</td>
<td class="py-3">${item['manager']}</td>
<td class="py-3">${item['manager_name']} ${item['manager_surname']}</td>
<td class="py-3" style='text-align: right;'>${format_number(item['capacity'],2)}</td>
<td class="py-3">${item['product']}</td>
<td class="py-3">${item['unique_product']}</td>
<td class="py-3">
<span class="badge bg-${item['bg_class']}">${item.status}</span>
${(item['status']==1)?'<span class="badge bg-success">Active</span>':'<span class="badge bg-secondary">Inactive</span>'}
</td>
<td class="py-3">
<a href="<?php echo $server_url?>inventory/manage_warehouse.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
@@ -371,6 +395,7 @@
$(async function() {
try {
await retrieve_stats();
await retrieve_warehouse();
await retrieve_storage();
} catch (e) {