more OOP and restructure utils
This commit is contained in:
@@ -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})";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
?>
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
?>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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
@@ -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 -->
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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));
|
||||
|
||||
?>
|
||||
@@ -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));
|
||||
|
||||
?>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user