[OOP] contact,inventory,ics,dashboard
This commit is contained in:
@@ -182,4 +182,192 @@ class ContactManager {
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Contact type queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Full contact type list.
|
||||
*/
|
||||
public function getContactTypeList(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_contact_type
|
||||
WHERE company_id = :company_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single contact type row by ID.
|
||||
*/
|
||||
public function getContactTypeById(int $id): array|false
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_contact_type
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a contact type.
|
||||
* Pass $data['id'] > 0 for update, 0 for insert.
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*/
|
||||
public function saveContactType(array $data, array $logging): void
|
||||
{
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT `log` FROM md_contact_type
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true) ?: [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
':company_id' => $this->company_id,
|
||||
':contact_type' => $data['contact_type'],
|
||||
':description' => $data['description'],
|
||||
':status' => (int)$data['status'],
|
||||
':log' => json_encode($table_log),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$params[':id'] = $id;
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_contact_type SET
|
||||
`contact_type` = :contact_type,
|
||||
`description` = :description,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute($params);
|
||||
} else {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO md_contact_type
|
||||
(company_id, contact_type, `description`, `status`, `log`)
|
||||
VALUES
|
||||
(:company_id, :contact_type, :description, :status, :log)"
|
||||
)->execute($params);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Contact queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Full contact list.
|
||||
*/
|
||||
public function getContactList(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_contact
|
||||
WHERE company_id = :company_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single contact row by ID.
|
||||
*/
|
||||
public function getContactById(int $id): array|false
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_contact
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search contacts by name keyword — for autocomplete.
|
||||
*/
|
||||
public function searchContact(string $keyword): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_contact
|
||||
WHERE company_id = :company_id
|
||||
AND contact_name LIKE :keyword
|
||||
LIMIT 50"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':keyword' => '%' . $keyword . '%',
|
||||
]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert or update a contact.
|
||||
* Pass $data['id'] > 0 for update, 0 for insert.
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*/
|
||||
public function saveContact(array $data, array $logging, string $contact_image): void
|
||||
{
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT `log` FROM md_contact
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true) ?: [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
':company_id' => $this->company_id,
|
||||
':contact_name' => $data['contact_name'],
|
||||
':tax_id' => $data['tax_id'],
|
||||
':organization' => $data['organization'],
|
||||
':branch' => $data['branch'],
|
||||
':contact_type' => (int)$data['contact_type'],
|
||||
':billing_address' => $data['billing_address'],
|
||||
':shipping_location' => $data['shipping_location'],
|
||||
':shipping_address' => $data['shipping_address'],
|
||||
':remark' => $data['remark'],
|
||||
':contact_image' => $contact_image,
|
||||
':status' => (int)$data['status'],
|
||||
':log' => json_encode($table_log),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$params[':id'] = $id;
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_contact SET
|
||||
`contact_name` = :contact_name,
|
||||
`tax_id` = :tax_id,
|
||||
`organization` = :organization,
|
||||
`branch` = :branch,
|
||||
`contact_type` = :contact_type,
|
||||
`billing_address` = :billing_address,
|
||||
`shipping_location` = :shipping_location,
|
||||
`shipping_address` = :shipping_address,
|
||||
`remark` = :remark,
|
||||
`contact_image` = :contact_image,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute($params);
|
||||
} else {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO md_contact
|
||||
(company_id, contact_name, tax_id, organization, branch, contact_type,
|
||||
billing_address, shipping_location, shipping_address, remark, contact_image, status, log)
|
||||
VALUES
|
||||
(:company_id, :contact_name, :tax_id, :organization, :branch, :contact_type,
|
||||
:billing_address, :shipping_location, :shipping_address, :remark, :contact_image, :status, :log)"
|
||||
)->execute($params);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,4 +183,240 @@ class ProductManager {
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Product queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Full product list with current warehouse balance.
|
||||
*/
|
||||
public function getProductList(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"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 a.company_id = :company_id
|
||||
GROUP BY a.id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single product row by ID.
|
||||
*/
|
||||
public function getProductById(int $id): array|false
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_product
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search products by SKU keyword — for autocomplete.
|
||||
*/
|
||||
public function searchProduct(string $keyword): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_product
|
||||
WHERE company_id = :company_id
|
||||
AND sku LIKE :keyword
|
||||
LIMIT 50"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':keyword' => '%' . $keyword . '%',
|
||||
]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Product category queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Full category list with product count per category.
|
||||
*/
|
||||
public function getCategoryList(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"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 a.company_id = :company_id
|
||||
GROUP BY a.id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single category row by ID.
|
||||
*/
|
||||
public function getCategoryById(int $id): array|false
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_product_category
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a product category.
|
||||
* Pass $data['id'] > 0 for update, 0 for insert.
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @throws Exception on validation failure
|
||||
*/
|
||||
public function saveCategory(array $data, array $logging): void
|
||||
{
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT `log` FROM md_product_category
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true) ?: [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
':company_id' => $this->company_id,
|
||||
':category' => $data['category'],
|
||||
':slug' => $data['slug'],
|
||||
':description' => $data['description'],
|
||||
':status' => (int)$data['status'],
|
||||
':log' => json_encode($table_log),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$params[':id'] = $id;
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_product_category SET
|
||||
`category` = :category,
|
||||
`slug` = :slug,
|
||||
`description` = :description,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute($params);
|
||||
} else {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO md_product_category
|
||||
(company_id, category, slug, `description`, `status`, `log`)
|
||||
VALUES
|
||||
(:company_id, :category, :slug, :description, :status, :log)"
|
||||
)->execute($params);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Product write
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Insert or update a product.
|
||||
* Pass $data['id'] > 0 for update, 0 for insert.
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*/
|
||||
public function saveProduct(array $data, array $logging, string $product_image): void
|
||||
{
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT `log` FROM md_product
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true) ?: [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
':company_id' => $this->company_id,
|
||||
':product_name' => $data['product_name'],
|
||||
':sku' => $data['sku'],
|
||||
':price' => $data['price'],
|
||||
':min_stock' => $data['min_stock'],
|
||||
':reorder_point' => $data['reorder_point'],
|
||||
':category' => $data['category'],
|
||||
':product_image' => $product_image,
|
||||
':description' => $data['description'],
|
||||
':status' => (int)$data['status'],
|
||||
':log' => json_encode($table_log),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$params[':id'] = $id;
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_product SET
|
||||
product_name = :product_name,
|
||||
sku = :sku,
|
||||
price = :price,
|
||||
min_stock = :min_stock,
|
||||
reorder_point = :reorder_point,
|
||||
category = :category,
|
||||
product_image = :product_image,
|
||||
`description` = :description,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute($params);
|
||||
} else {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO md_product
|
||||
(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, :reorder_point,
|
||||
:category, :product_image, :description, :status, :log)"
|
||||
)->execute($params);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Full rack list with warehouse name, product info and occupancy status.
|
||||
*/
|
||||
public function getRackOccupancy(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT
|
||||
r.id,
|
||||
r.zone,
|
||||
r.aisle,
|
||||
r.rack,
|
||||
r.product_sku,
|
||||
r.td_stock_id,
|
||||
mw.warehouse_name,
|
||||
mw.id AS warehouse_id,
|
||||
p.product_name,
|
||||
CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status
|
||||
FROM md_rack r
|
||||
INNER JOIN md_warehouse mw
|
||||
ON mw.company_id = r.company_id
|
||||
AND mw.id = r.warehouse
|
||||
LEFT JOIN md_product p
|
||||
ON p.company_id = r.company_id
|
||||
AND p.sku = r.product_sku
|
||||
WHERE r.company_id = :company_id
|
||||
ORDER BY mw.warehouse_name, r.zone,
|
||||
CAST(r.aisle AS UNSIGNED), r.aisle,
|
||||
CAST(r.rack AS UNSIGNED), r.rack"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
@@ -488,23 +488,29 @@ class ReportManager
|
||||
$start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00');
|
||||
$end = $now->format('Y-m-d 23:59:59');
|
||||
|
||||
$sth = $this->pdo->query(
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DATE_FORMAT(date, '%b') AS label,
|
||||
DATE_FORMAT(date, '%Y-%m') AS sort_key,
|
||||
ROUND(SUM(`in`), 2) AS stock_in,
|
||||
ROUND(SUM(`out`), 2) AS stock_out
|
||||
FROM `{$table}`
|
||||
WHERE company_id = {$this->companyId}
|
||||
AND date >= '{$start}' AND date <= '{$end}'
|
||||
WHERE company_id = :company_id
|
||||
AND date >= :start AND date <= :end
|
||||
GROUP BY sort_key, label
|
||||
ORDER BY sort_key ASC"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->companyId,
|
||||
':start' => $start,
|
||||
':end' => $end,
|
||||
]);
|
||||
|
||||
$dataByMonth = [];
|
||||
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$dataByMonth[$row['sort_key']] = $row;
|
||||
}
|
||||
|
||||
$labels = []; $stock_in = []; $stock_out = [];
|
||||
$labels = $stock_in = $stock_out = [];
|
||||
for ($i = 11; $i >= 0; $i--) {
|
||||
$dt = (clone $now)->modify("-{$i} months");
|
||||
$key = $dt->format('Y-m');
|
||||
@@ -525,29 +531,36 @@ class ReportManager
|
||||
$start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00');
|
||||
$end = $now->format('Y-m-d 23:59:59');
|
||||
|
||||
$sth = $this->pdo->query(
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT ROUND(COALESCE(SUM(`in`) - SUM(`out`), 0), 2) AS balance
|
||||
FROM `{$table}`
|
||||
WHERE company_id = {$this->companyId} AND date < '{$start}'"
|
||||
WHERE company_id = :company_id AND date < :start"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId, ':start' => $start]);
|
||||
$running = (float)$sth->fetchColumn();
|
||||
|
||||
$sth = $this->pdo->query(
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DATE_FORMAT(date, '%Y-%m') AS sort_key,
|
||||
ROUND(SUM(`in`), 2) AS stock_in,
|
||||
ROUND(SUM(`out`), 2) AS stock_out
|
||||
FROM `{$table}`
|
||||
WHERE company_id = {$this->companyId}
|
||||
AND date >= '{$start}' AND date <= '{$end}'
|
||||
WHERE company_id = :company_id
|
||||
AND date >= :start AND date <= :end
|
||||
GROUP BY sort_key
|
||||
ORDER BY sort_key ASC"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->companyId,
|
||||
':start' => $start,
|
||||
':end' => $end,
|
||||
]);
|
||||
|
||||
$dataByMonth = [];
|
||||
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$dataByMonth[$row['sort_key']] = $row;
|
||||
}
|
||||
|
||||
$labels = []; $balance = [];
|
||||
$labels = $balance = [];
|
||||
for ($i = 11; $i >= 0; $i--) {
|
||||
$dt = (clone $now)->modify("-{$i} months");
|
||||
$key = $dt->format('Y-m');
|
||||
@@ -566,18 +579,19 @@ class ReportManager
|
||||
$table = $this->resolveWarehouseTable($warehouse_id);
|
||||
if (!$table) return [];
|
||||
|
||||
$sth = $this->pdo->query(
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.date, a.product_sku,
|
||||
a.`in` AS stock_in, a.`out` AS stock_out,
|
||||
a.type, a.description, b.product_name
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_product b
|
||||
ON a.company_id = b.company_id AND a.product_sku = b.sku
|
||||
WHERE a.company_id = {$this->companyId}
|
||||
WHERE a.company_id = :company_id
|
||||
AND (a.`in` > 0 OR a.`out` > 0)
|
||||
ORDER BY a.date DESC, a.id DESC
|
||||
LIMIT 10"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
|
||||
$activities = [];
|
||||
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
@@ -694,6 +708,207 @@ class ReportManager
|
||||
return $items;
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Warehouse balance summary
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Aggregate total_in, total_out and warehouse count across all warehouses.
|
||||
*/
|
||||
public function getWarehouseBalanceSummary(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT
|
||||
SUM(total_in) AS total_in,
|
||||
SUM(total_out) AS total_out,
|
||||
COUNT(DISTINCT warehouse_id) AS total_warehouse
|
||||
FROM warehouse_balance
|
||||
WHERE company_id = :company_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Dashboard report queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* All stock movements for a given product_sku + lot_number across all warehouses,
|
||||
* sorted by date DESC.
|
||||
*/
|
||||
public function getLotStockLog(string $product_sku, string $lot_number): array
|
||||
{
|
||||
if (!$product_sku || !$lot_number) return [];
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, warehouse_name FROM md_warehouse
|
||||
WHERE company_id = :company_id AND status = 1"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
$warehouses = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($warehouses as $wh) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
|
||||
$table = "td_stock_{$safe}";
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT
|
||||
s.id,
|
||||
s.date,
|
||||
s.type,
|
||||
s.zone,
|
||||
s.aisle,
|
||||
s.rack,
|
||||
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
|
||||
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
|
||||
s.serial_number,
|
||||
s.description,
|
||||
'{$wh['warehouse_name']}' AS warehouse_name
|
||||
FROM `{$table}` s
|
||||
WHERE s.company_id = :company_id
|
||||
AND s.product_sku = :product_sku
|
||||
AND s.lot_number = :lot_number
|
||||
ORDER BY s.date DESC"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->companyId,
|
||||
':product_sku' => $product_sku,
|
||||
':lot_number' => $lot_number,
|
||||
]);
|
||||
$rows = array_merge($rows, $sth->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
usort($rows, fn($a, $b) => strcmp($b['date'], $a['date']));
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* All lots with running balance, expiry status, and days remaining.
|
||||
* Returns rows plus summary counts (total, active, expired, near).
|
||||
*/
|
||||
public function getProductLots(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT
|
||||
l.id,
|
||||
l.product_sku,
|
||||
l.lot_number,
|
||||
l.expiry_date,
|
||||
l.description,
|
||||
p.product_name,
|
||||
DATEDIFF(l.expiry_date, CURDATE()) AS days_remaining,
|
||||
COALESCE(
|
||||
(SELECT ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2)
|
||||
FROM warehouse_balance wb
|
||||
WHERE wb.company_id = l.company_id
|
||||
AND wb.product_sku = l.product_sku),
|
||||
0
|
||||
) AS balance
|
||||
FROM md_lot l
|
||||
INNER JOIN md_product p
|
||||
ON p.company_id = l.company_id
|
||||
AND p.sku = l.product_sku
|
||||
WHERE l.company_id = :company_id
|
||||
ORDER BY l.product_sku, l.expiry_date ASC"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$active = $expired = $near = 0;
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$days = (int)$row['days_remaining'];
|
||||
$balance = (float)$row['balance'];
|
||||
|
||||
if ($balance > 0) $active++;
|
||||
if ($days < 0) $expired++;
|
||||
if ($days >= 0 && $days <= 30) $near++;
|
||||
|
||||
if ($days < 0) $row['status'] = 'expired';
|
||||
elseif ($days <= 7) $row['status'] = 'critical';
|
||||
elseif ($days <= 30) $row['status'] = 'near';
|
||||
else $row['status'] = 'ok';
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return [
|
||||
'rows' => $rows,
|
||||
'total' => count($rows),
|
||||
'active' => $active,
|
||||
'expired' => $expired,
|
||||
'near' => $near,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rack activity log for a given rack_id, with user name.
|
||||
*/
|
||||
public function getRackLog(int $rack_id): array
|
||||
{
|
||||
if (!$rack_id) return [];
|
||||
|
||||
$db = $this->pdo->query("SELECT DATABASE()")->fetchColumn();
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT
|
||||
rl.id,
|
||||
rl.dt,
|
||||
rl.action,
|
||||
rl.product_sku,
|
||||
rl.td_stock_id,
|
||||
rl.login,
|
||||
u.name AS user_name
|
||||
FROM md_rack_log rl
|
||||
LEFT JOIN {$db}.user u
|
||||
ON u.user_id = rl.user_id
|
||||
WHERE rl.company_id = :company_id
|
||||
AND rl.md_rack_id = :rack_id
|
||||
ORDER BY rl.dt DESC"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->companyId,
|
||||
':rack_id' => $rack_id,
|
||||
]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Full rack list with warehouse name, product info and occupancy status.
|
||||
*/
|
||||
public function getRackOccupancy(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT
|
||||
r.id,
|
||||
r.zone,
|
||||
r.aisle,
|
||||
r.rack,
|
||||
r.product_sku,
|
||||
r.td_stock_id,
|
||||
mw.warehouse_name,
|
||||
mw.id AS warehouse_id,
|
||||
p.product_name,
|
||||
CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status
|
||||
FROM md_rack r
|
||||
INNER JOIN md_warehouse mw
|
||||
ON mw.company_id = r.company_id
|
||||
AND mw.id = r.warehouse
|
||||
LEFT JOIN md_product p
|
||||
ON p.company_id = r.company_id
|
||||
AND p.sku = r.product_sku
|
||||
WHERE r.company_id = :company_id
|
||||
ORDER BY mw.warehouse_name, r.zone,
|
||||
CAST(r.aisle AS UNSIGNED), r.aisle,
|
||||
CAST(r.rack AS UNSIGNED), r.rack"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* StockManager
|
||||
*
|
||||
* Encapsulates read operations for ICS stock transactions:
|
||||
* - Stock list by type (in / out / transfer)
|
||||
* - Single record retrieval for stock_in, stock_out, transfer
|
||||
*
|
||||
* Write operations (manage_stock_in, manage_stock_out, manage_stock_transfer)
|
||||
* are handled in their engine files via WarehouseManager.
|
||||
*/
|
||||
class StockManager {
|
||||
|
||||
private $pdo;
|
||||
private $company_id;
|
||||
|
||||
public function __construct($pdo, $company_id) {
|
||||
$this->pdo = $pdo;
|
||||
$this->company_id = $company_id;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Private helpers
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve warehouse name by ID and return the td_stock_<wh> table name.
|
||||
* Throws if warehouse not found.
|
||||
*/
|
||||
private function resolveTable(int $warehouse_id): string
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT warehouse_name FROM md_warehouse
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
|
||||
$name = $sth->fetchColumn();
|
||||
|
||||
if (!$name) {
|
||||
throw new Exception("Warehouse not found.");
|
||||
}
|
||||
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $name);
|
||||
return "td_stock_{$safe}";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Stock list queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* List all stock records of a given type for a warehouse.
|
||||
* type: 'in' | 'out' | 'transfer'
|
||||
*/
|
||||
public function getStockList(int $warehouse_id, string $type): array
|
||||
{
|
||||
$table = $this->resolveTable($warehouse_id);
|
||||
$column = $type === 'out' ? 'ROUND(a.out, 2)' : 'ROUND(a.in, 2)';
|
||||
|
||||
// transfer list shows outbound side only (out > 0)
|
||||
$extra_cond = ($type === 'transfer') ? 'AND a.out > 0' : '';
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, {$column} AS quantity, b.product_name
|
||||
FROM `{$table}` 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
|
||||
AND a.type = :type
|
||||
{$extra_cond}
|
||||
ORDER BY a.date DESC"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':type' => $type,
|
||||
]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Single record retrieval
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fetch a single stock_in record with product and contact name,
|
||||
* plus lot expiry date if applicable.
|
||||
*/
|
||||
public function getStockInById(int $warehouse_id, int $id): array|false
|
||||
{
|
||||
$table = $this->resolveTable($warehouse_id);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, a.in AS quantity,
|
||||
b.contact_name,
|
||||
c.product_name,
|
||||
d.expiry_date
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b
|
||||
ON a.company_id = b.company_id AND a.contact_id = b.id
|
||||
LEFT JOIN md_product c
|
||||
ON a.company_id = c.company_id AND a.product_sku = c.sku
|
||||
LEFT JOIN md_lot d
|
||||
ON a.company_id = d.company_id
|
||||
AND a.product_sku = d.product_sku
|
||||
AND a.lot_number = d.lot_number
|
||||
WHERE a.company_id = :company_id AND a.id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single stock_out record with product and contact name.
|
||||
*/
|
||||
public function getStockOutById(int $warehouse_id, int $id): array|false
|
||||
{
|
||||
$table = $this->resolveTable($warehouse_id);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, a.out AS quantity,
|
||||
b.contact_name,
|
||||
c.product_name
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b
|
||||
ON a.company_id = b.company_id AND a.contact_id = b.id
|
||||
LEFT JOIN md_product c
|
||||
ON a.company_id = c.company_id AND a.product_sku = c.sku
|
||||
WHERE a.company_id = :company_id AND a.id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a transfer record pair — the outbound row plus its paired
|
||||
* inbound row resolved via ref_warehouse + uuid.
|
||||
* Returns the outbound row with a 'ref' key containing the inbound row.
|
||||
*/
|
||||
public function getTransferById(int $warehouse_id, int $id): array|false
|
||||
{
|
||||
$table = $this->resolveTable($warehouse_id);
|
||||
|
||||
// Fetch the outbound (from) row
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, a.out AS quantity,
|
||||
b.contact_name,
|
||||
c.product_name
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b
|
||||
ON a.company_id = b.company_id AND a.contact_id = b.id
|
||||
LEFT JOIN md_product c
|
||||
ON a.company_id = c.company_id AND a.product_sku = c.sku
|
||||
WHERE a.company_id = :company_id AND a.id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
$output = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$output) return false;
|
||||
|
||||
// Resolve the inbound (to) row via ref_warehouse + uuid
|
||||
$to_warehouse_id = (int)$output['ref_warehouse'];
|
||||
$to_table = $this->resolveTable($to_warehouse_id);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, a.in AS quantity,
|
||||
b.contact_name,
|
||||
c.product_name
|
||||
FROM `{$to_table}` a
|
||||
LEFT JOIN md_contact b
|
||||
ON a.company_id = b.company_id AND a.contact_id = b.id
|
||||
LEFT JOIN md_product c
|
||||
ON a.company_id = b.company_id AND a.product_sku = c.sku
|
||||
WHERE a.company_id = :company_id AND a.uuid = :uuid"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':uuid' => $output['uuid']]);
|
||||
$output['ref'] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Stock write operations
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Insert or update a stock_in record.
|
||||
* On insert: upserts md_lot, inserts td_stock row, occupies rack, adjusts balance.
|
||||
* On update: updates metadata (contact, description, log) only.
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*/
|
||||
public function saveStockIn(array $data, array $logging, string $uuid): void
|
||||
{
|
||||
$id = (int)$data['id'];
|
||||
$warehouse_id = (int)$data['warehouse'];
|
||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||
|
||||
$ctx = $whMgmt->getStockContext($warehouse_id, $id);
|
||||
$row = $ctx['row'] ?? [];
|
||||
$table = $ctx['table'];
|
||||
$table_log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `$table` SET
|
||||
`contact_id` = :contact_id,
|
||||
`description` = :description,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':id' => $id,
|
||||
':company_id' => $this->company_id,
|
||||
':contact_id' => (int)$data['contact_id'],
|
||||
':description' => $data['description'],
|
||||
':log' => json_encode($table_log),
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
||||
$lot_number = $data['lot_number'] ?: null;
|
||||
$expiry_date = $data['expiry_date'] ?: null;
|
||||
|
||||
// Upsert md_lot if lot + expiry provided
|
||||
if ($lot_number && $expiry_date) {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO md_lot (company_id, product_sku, lot_number, expiry_date)
|
||||
VALUES (:company_id, :product_sku, :lot_number, :expiry_date)
|
||||
ON DUPLICATE KEY UPDATE expiry_date = expiry_date"
|
||||
)->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':product_sku' => $data['product_sku'],
|
||||
':lot_number' => $lot_number,
|
||||
':expiry_date' => $expiry_date,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$table`
|
||||
(uuid, company_id, `date`, product_sku, `in`, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'in', :lot_number, :serial_number)"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
':company_id' => $this->company_id,
|
||||
':date' => date('Y-m-d H:i:s'),
|
||||
':product_sku' => $data['product_sku'],
|
||||
':quantity' => $data['quantity'],
|
||||
':zone' => $data['zone'],
|
||||
':aisle' => $data['aisle'],
|
||||
':rack' => $data['rack'],
|
||||
':contact_id' => (int)$data['contact_id'],
|
||||
':description' => $data['description'],
|
||||
':log' => json_encode($table_log),
|
||||
':lot_number' => $lot_number,
|
||||
':serial_number' => $data['serial_number'] ?: null,
|
||||
]);
|
||||
|
||||
$td_stock_id = (int)$this->pdo->lastInsertId();
|
||||
|
||||
$whMgmt->occupyRack(
|
||||
$warehouse_id,
|
||||
$data['zone'], $data['aisle'], $data['rack'],
|
||||
$data['product_sku'],
|
||||
$td_stock_id
|
||||
);
|
||||
|
||||
$whMgmt->adjustBalance(
|
||||
'in',
|
||||
$warehouse_id,
|
||||
$data['product_sku'],
|
||||
$row['in'] ?? 0,
|
||||
$data['quantity']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a stock_out record.
|
||||
* On insert: validates rack, inserts td_stock row, releases rack, adjusts balance.
|
||||
* On update: updates metadata (contact, description, log) only.
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*/
|
||||
public function saveStockOut(array $data, array $logging, string $uuid): void
|
||||
{
|
||||
$id = (int)$data['id'];
|
||||
$warehouse_id = (int)$data['warehouse'];
|
||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||
|
||||
$ctx = $whMgmt->getStockContext($warehouse_id, $id);
|
||||
$row = $ctx['row'] ?? [];
|
||||
$table = $ctx['table'];
|
||||
$table_log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `$table` SET
|
||||
`contact_id` = :contact_id,
|
||||
`description` = :description,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':id' => $id,
|
||||
':company_id' => $this->company_id,
|
||||
':contact_id' => (int)$data['contact_id'],
|
||||
':description' => $data['description'],
|
||||
':log' => json_encode($table_log),
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
||||
$source_stock = $whMgmt->getRackStock(
|
||||
$warehouse_id,
|
||||
$data['zone'], $data['aisle'], $data['rack']
|
||||
);
|
||||
|
||||
if (!$source_stock) {
|
||||
throw new Exception(
|
||||
"Rack {$data['zone']}-{$data['aisle']}-{$data['rack']} is empty — nothing to take out."
|
||||
);
|
||||
}
|
||||
|
||||
if ($source_stock['product_sku'] !== $data['product_sku']) {
|
||||
throw new Exception(
|
||||
"Rack holds {$source_stock['product_sku']}, not {$data['product_sku']}."
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($data['lot_number']) && $source_stock['lot_number'] !== $data['lot_number']) {
|
||||
throw new Exception(
|
||||
"Rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($data['serial_number']) && $source_stock['serial_number'] !== $data['serial_number']) {
|
||||
throw new Exception(
|
||||
"Rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
$quantity = (int)$source_stock['in'];
|
||||
$ref_id = (int)$source_stock['id'];
|
||||
$lot_number = $source_stock['lot_number'] ?? null;
|
||||
$serial_number = $source_stock['serial_number'] ?? null;
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$table`
|
||||
(uuid, company_id, `date`, product_sku, `out`, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, ref_id, lot_number, serial_number)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number)"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
':company_id' => $this->company_id,
|
||||
':date' => date('Y-m-d H:i:s'),
|
||||
':product_sku' => $data['product_sku'],
|
||||
':quantity' => $quantity,
|
||||
':zone' => $data['zone'],
|
||||
':aisle' => $data['aisle'],
|
||||
':rack' => $data['rack'],
|
||||
':contact_id' => (int)$data['contact_id'],
|
||||
':description' => $data['description'],
|
||||
':log' => json_encode($table_log),
|
||||
':ref_id' => $ref_id,
|
||||
':lot_number' => $lot_number,
|
||||
':serial_number' => $serial_number,
|
||||
]);
|
||||
|
||||
$whMgmt->releaseRack(
|
||||
$warehouse_id,
|
||||
$data['zone'], $data['aisle'], $data['rack']
|
||||
);
|
||||
|
||||
$whMgmt->adjustBalance(
|
||||
'out',
|
||||
$warehouse_id,
|
||||
$data['product_sku'],
|
||||
$row['out'] ?? 0,
|
||||
$quantity
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a stock transfer record pair.
|
||||
* On insert: validates source rack, inserts paired td_stock rows, moves rack state, adjusts both balances.
|
||||
* On update: updates metadata (contact, description, log) on both rows only.
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*/
|
||||
public function saveStockTransfer(array $data, array $logging, string $uuid): void
|
||||
{
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||
|
||||
$contact_id = (int)($data['contact_id'] ?? 0);
|
||||
$description = ($data['description'] ?? '');
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$from_warehouse = (int)($data['warehouse_from'] ?? 0);
|
||||
$to_warehouse = (int)($data['warehouse_to'] ?? 0);
|
||||
|
||||
$from_ctx = $whMgmt->getStockContext($from_warehouse, $id);
|
||||
$from_row = $from_ctx['row'];
|
||||
|
||||
if (!$from_row) {
|
||||
throw new Exception("Transfer record not found.");
|
||||
}
|
||||
|
||||
$to_id = (int)$from_row['ref_id'];
|
||||
$to_ctx = $whMgmt->getStockContext($to_warehouse, $to_id);
|
||||
$to_row = $to_ctx['row'];
|
||||
|
||||
if (!$to_row) {
|
||||
throw new Exception("Paired destination record missing — data integrity issue.");
|
||||
}
|
||||
|
||||
$from_log = json_decode($from_row['log'] ?? '[]', true) ?: [];
|
||||
$from_log[] = $logging;
|
||||
$to_log = json_decode($to_row['log'] ?? '[]', true) ?: [];
|
||||
$to_log[] = $logging;
|
||||
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `{$from_ctx['table']}` SET
|
||||
contact_id = :contact_id,
|
||||
description = :description,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':contact_id' => $contact_id,
|
||||
':description' => $description,
|
||||
':log' => json_encode($from_log),
|
||||
':id' => $id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `{$to_ctx['table']}` SET
|
||||
contact_id = :contact_id,
|
||||
description = :description,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':contact_id' => $contact_id,
|
||||
':description' => $description,
|
||||
':log' => json_encode($to_log),
|
||||
':id' => $to_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$from_warehouse = (int)$data['warehouse_from'];
|
||||
$from_zone = $data['zone_from'];
|
||||
$from_aisle = $data['aisle_from'];
|
||||
$from_rack = $data['rack_from'];
|
||||
$to_warehouse = (int)$data['warehouse_to'];
|
||||
$to_zone = $data['zone_to'];
|
||||
$to_aisle = $data['aisle_to'];
|
||||
$to_rack = $data['rack_to'];
|
||||
|
||||
$source_stock = $whMgmt->getRackStock(
|
||||
$from_warehouse, $from_zone, $from_aisle, $from_rack
|
||||
);
|
||||
|
||||
if (!$source_stock) {
|
||||
throw new Exception("Source rack {$from_zone}-{$from_aisle}-{$from_rack} is empty.");
|
||||
}
|
||||
|
||||
if ($source_stock['product_sku'] !== $data['product_sku']) {
|
||||
throw new Exception(
|
||||
"Source rack holds {$source_stock['product_sku']}, not {$data['product_sku']}."
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($data['lot_number']) && $source_stock['lot_number'] !== $data['lot_number']) {
|
||||
throw new Exception(
|
||||
"Source rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($data['serial_number']) && $source_stock['serial_number'] !== $data['serial_number']) {
|
||||
throw new Exception(
|
||||
"Source rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
$quantity = (int)$source_stock['in'];
|
||||
$lot_number = $source_stock['lot_number'] ?? null;
|
||||
$serial_number = $source_stock['serial_number'] ?? null;
|
||||
|
||||
$from_table = $whMgmt->getStockContext($from_warehouse, 0)['table'];
|
||||
$to_table = $whMgmt->getStockContext($to_warehouse, 0)['table'];
|
||||
$table_log = [$logging];
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$from_table`
|
||||
(uuid, company_id, `date`, product_sku, `out`,
|
||||
ref_warehouse, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:ref_warehouse, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number)"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
':company_id' => $this->company_id,
|
||||
':date' => date('Y-m-d H:i:s'),
|
||||
':product_sku' => $data['product_sku'],
|
||||
':quantity' => $quantity,
|
||||
':ref_warehouse' => $to_warehouse,
|
||||
':zone' => $from_zone,
|
||||
':aisle' => $from_aisle,
|
||||
':rack' => $from_rack,
|
||||
':contact_id' => $contact_id,
|
||||
':description' => $description,
|
||||
':log' => json_encode($table_log),
|
||||
':lot_number' => $lot_number,
|
||||
':serial_number' => $serial_number,
|
||||
]);
|
||||
$from_stock_id = (int)$this->pdo->lastInsertId();
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$to_table`
|
||||
(uuid, company_id, `date`, product_sku, `in`,
|
||||
ref_warehouse, ref_id, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:ref_warehouse, :ref_id, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number)"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
':company_id' => $this->company_id,
|
||||
':date' => date('Y-m-d H:i:s'),
|
||||
':product_sku' => $data['product_sku'],
|
||||
':quantity' => $quantity,
|
||||
':ref_warehouse' => $from_warehouse,
|
||||
':ref_id' => $from_stock_id,
|
||||
':zone' => $to_zone,
|
||||
':aisle' => $to_aisle,
|
||||
':rack' => $to_rack,
|
||||
':contact_id' => $contact_id,
|
||||
':description' => $description,
|
||||
':log' => json_encode($table_log),
|
||||
':lot_number' => $lot_number,
|
||||
':serial_number' => $serial_number,
|
||||
]);
|
||||
$to_stock_id = (int)$this->pdo->lastInsertId();
|
||||
|
||||
// Back-fill ref_id on from row so both rows point at each other
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `$from_table` SET ref_id = :ref_id
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':ref_id' => $to_stock_id,
|
||||
':id' => $from_stock_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
|
||||
$whMgmt->releaseRack($from_warehouse, $from_zone, $from_aisle, $from_rack);
|
||||
$whMgmt->occupyRack($to_warehouse, $to_zone, $to_aisle, $to_rack, $data['product_sku'], $to_stock_id);
|
||||
|
||||
$whMgmt->adjustBalance('out', $from_warehouse, $data['product_sku'], 0, $quantity);
|
||||
$whMgmt->adjustBalance('in', $to_warehouse, $data['product_sku'], 0, $quantity);
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
@@ -1272,6 +1272,430 @@ class WarehouseManager {
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Warehouse list queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* List warehouses filtered by rack occupancy type.
|
||||
* type='to' → only warehouses with at least one empty rack
|
||||
* type='from' → only warehouses holding the given product_sku
|
||||
* other → all warehouses
|
||||
* Pass $id > 0 to bypass the filter (edit flows).
|
||||
*/
|
||||
public function getWarehouseList(string $type, string $product_sku = '', int $id = 0): array
|
||||
{
|
||||
$product_sku_filter = '';
|
||||
$params = [':company_id' => $this->company_id];
|
||||
|
||||
if (!$id) {
|
||||
if ($type === 'to') {
|
||||
$product_sku_filter = 'AND r.product_sku IS NULL';
|
||||
} elseif ($type === 'from') {
|
||||
$product_sku_filter = 'AND r.product_sku = :product_sku';
|
||||
$params[':product_sku'] = $product_sku;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT w.id, w.warehouse_name
|
||||
FROM md_warehouse w
|
||||
WHERE w.company_id = :company_id
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM md_rack r
|
||||
WHERE r.company_id = w.company_id
|
||||
AND r.warehouse = w.id
|
||||
$product_sku_filter
|
||||
)
|
||||
ORDER BY w.warehouse_name"
|
||||
);
|
||||
$sth->execute($params);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full warehouse list with rack stats and manager name — for inventory listing.
|
||||
*/
|
||||
public function getWarehouseListAll(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT
|
||||
a.*,
|
||||
COALESCE(r.total_racks, 0) AS capacity,
|
||||
COALESCE(r.occupied_racks, 0) AS space_used,
|
||||
COALESCE(r.unique_product, 0) AS unique_product,
|
||||
c.name AS manager_name,
|
||||
c.surname AS manager_surname
|
||||
FROM md_warehouse a
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
company_id, warehouse,
|
||||
COUNT(*) AS total_racks,
|
||||
COUNT(product_sku) AS occupied_racks,
|
||||
COUNT(DISTINCT product_sku) AS unique_product
|
||||
FROM md_rack
|
||||
GROUP BY company_id, warehouse
|
||||
) r ON a.company_id = r.company_id AND a.id = r.warehouse
|
||||
LEFT JOIN wms.user c ON a.manager = c.user_id
|
||||
WHERE a.company_id = :company_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single warehouse row by ID.
|
||||
*/
|
||||
public function getWarehouseById(int $id): array|false
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_warehouse
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Lot / Serial queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Search md_lot by product_sku + keyword — for lot number autocomplete.
|
||||
*/
|
||||
public function getLotList(string $product_sku, string $keyword): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT *
|
||||
FROM md_lot
|
||||
WHERE company_id = :company_id
|
||||
AND product_sku = :product_sku
|
||||
AND lot_number LIKE :keyword
|
||||
ORDER BY lot_number ASC
|
||||
LIMIT 50"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':product_sku' => $product_sku,
|
||||
':keyword' => '%' . $keyword . '%',
|
||||
]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return distinct active lots for a product_sku across all warehouses.
|
||||
* "Active" = rack is still occupied (product_sku IS NOT NULL in md_rack).
|
||||
*/
|
||||
public function getActiveLots(string $product_sku): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, warehouse_name FROM md_warehouse
|
||||
WHERE company_id = :company_id AND status = 1"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id]);
|
||||
$warehouses = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$active_lots = [];
|
||||
|
||||
foreach ($warehouses as $wh) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
|
||||
$table = "td_stock_{$safe}";
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT s.lot_number, l.expiry_date
|
||||
FROM `{$table}` s
|
||||
LEFT JOIN md_lot l
|
||||
ON l.company_id = s.company_id
|
||||
AND l.product_sku = s.product_sku
|
||||
AND l.lot_number = s.lot_number
|
||||
WHERE s.company_id = :company_id
|
||||
AND s.product_sku = :product_sku
|
||||
AND s.type = 'in'
|
||||
AND s.lot_number IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM md_rack r
|
||||
WHERE r.company_id = s.company_id
|
||||
AND r.td_stock_id = s.id
|
||||
AND r.product_sku IS NOT NULL
|
||||
)"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':product_sku' => $product_sku,
|
||||
]);
|
||||
|
||||
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$active_lots[$row['lot_number']] ??= $row;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($active_lots);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return distinct active serial numbers for a product_sku (+ optional lot)
|
||||
* across all warehouses.
|
||||
* "Active" = rack is still occupied.
|
||||
*/
|
||||
public function getActiveSerials(string $product_sku, string $lot_number = ''): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, warehouse_name FROM md_warehouse
|
||||
WHERE company_id = :company_id AND status = 1"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id]);
|
||||
$warehouses = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$active_serials = [];
|
||||
|
||||
foreach ($warehouses as $wh) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
|
||||
$table = "td_stock_{$safe}";
|
||||
|
||||
$sql = "SELECT DISTINCT s.serial_number
|
||||
FROM `{$table}` s
|
||||
WHERE s.company_id = :company_id
|
||||
AND s.product_sku = :product_sku
|
||||
AND s.type = 'in'
|
||||
AND s.serial_number IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM md_rack r
|
||||
WHERE r.company_id = s.company_id
|
||||
AND r.td_stock_id = s.id
|
||||
AND r.product_sku IS NOT NULL
|
||||
)";
|
||||
|
||||
$params = [
|
||||
':company_id' => $this->company_id,
|
||||
':product_sku' => $product_sku,
|
||||
];
|
||||
|
||||
if (!empty($lot_number)) {
|
||||
$sql .= ' AND s.lot_number = :lot_number';
|
||||
$params[':lot_number'] = $lot_number;
|
||||
}
|
||||
|
||||
$sth = $this->pdo->prepare($sql);
|
||||
$sth->execute($params);
|
||||
|
||||
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$active_serials[$row['serial_number']] = $row['serial_number'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($active_serials);
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Storage queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Full storage list with warehouse name.
|
||||
*/
|
||||
public function getStorageList(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, b.warehouse_name
|
||||
FROM md_storage a
|
||||
LEFT JOIN md_warehouse b
|
||||
ON a.company_id = b.company_id
|
||||
AND a.warehouse = b.id
|
||||
WHERE a.company_id = :company_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single storage row by ID.
|
||||
*/
|
||||
public function getStorageById(int $id): array|false
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_storage
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Warehouse write
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Insert or update a warehouse.
|
||||
* Creates td_stock_<name> table on insert.
|
||||
* Pass $data['id'] > 0 for update, 0 for insert.
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*/
|
||||
public function saveWarehouse(array $data, array $logging): void
|
||||
{
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT `log` FROM md_warehouse
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true) ?: [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
':company_id' => $this->company_id,
|
||||
':warehouse_name' => $data['warehouse_name'] ?? '',
|
||||
':location' => $data['location'] ?? '',
|
||||
':manager' => (int)($data['manager'] ?? 0),
|
||||
':description' => $data['description'] ?? '',
|
||||
':status' => (int)($data['status'] ?? 1),
|
||||
':log' => json_encode($table_log, JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$params[':id'] = $id;
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_warehouse SET
|
||||
`warehouse_name` = :warehouse_name,
|
||||
`location` = :location,
|
||||
`manager` = :manager,
|
||||
`description` = :description,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute($params);
|
||||
} else {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO md_warehouse
|
||||
(company_id, warehouse_name, location, manager, description, status, `log`)
|
||||
VALUES
|
||||
(:company_id, :warehouse_name, :location, :manager, :description, :status, :log)"
|
||||
)->execute($params);
|
||||
|
||||
// Create the per-warehouse stock table on insert
|
||||
if (!empty($data['warehouse_name'])) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $data['warehouse_name']);
|
||||
$this->pdo->exec("CREATE TABLE `td_stock_{$safe}` LIKE `td_stock`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert or update a storage record.
|
||||
* Pass $data['id'] > 0 for update, 0 for insert.
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
* Automatically syncs md_rack rows after save.
|
||||
*/
|
||||
public function saveStorage(array $data, array $logging): void
|
||||
{
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT `log` FROM md_storage
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true) ?: [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
':company_id' => $this->company_id,
|
||||
':warehouse' => $data['warehouse'] ?? '',
|
||||
':zone' => $data['zone'] ?? '',
|
||||
':aisle_from' => $data['aisle_from'] ?? '',
|
||||
':aisle_to' => $data['aisle_to'] ?? '',
|
||||
':rack_from' => $data['rack_from'] ?? '',
|
||||
':rack_to' => $data['rack_to'] ?? '',
|
||||
':description' => $data['description'] ?? '',
|
||||
':status' => (int)($data['status'] ?? 1),
|
||||
':log' => json_encode($table_log, JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$params[':id'] = $id;
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_storage SET
|
||||
warehouse = :warehouse,
|
||||
zone = :zone,
|
||||
aisle_from = :aisle_from,
|
||||
aisle_to = :aisle_to,
|
||||
rack_from = :rack_from,
|
||||
rack_to = :rack_to,
|
||||
description = :description,
|
||||
status = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute($params);
|
||||
|
||||
$storage_id = $id;
|
||||
} else {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO md_storage
|
||||
(company_id, warehouse, zone, aisle_from, aisle_to,
|
||||
rack_from, rack_to, description, status, `log`)
|
||||
VALUES
|
||||
(:company_id, :warehouse, :zone, :aisle_from, :aisle_to,
|
||||
:rack_from, :rack_to, :description, :status, :log)"
|
||||
)->execute($params);
|
||||
|
||||
$storage_id = (int)$this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// Sync md_rack rows to match the new range
|
||||
$this->syncRacks($storage_id, [
|
||||
'warehouse' => $data['warehouse'],
|
||||
'zone' => $data['zone'],
|
||||
'aisle_from' => $data['aisle_from'],
|
||||
'aisle_to' => $data['aisle_to'],
|
||||
'rack_from' => $data['rack_from'],
|
||||
'rack_to' => $data['rack_to'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* All distinct zones in a warehouse — for admin/read-only listing.
|
||||
*/
|
||||
public function getZonesAll(int $warehouse_id): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT zone FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse
|
||||
ORDER BY CAST(zone AS UNSIGNED), zone"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':warehouse' => $warehouse_id]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* All distinct aisles in a zone — for admin/read-only listing.
|
||||
*/
|
||||
public function getAislesAll(int $warehouse_id, string $zone): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT aisle FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone
|
||||
ORDER BY CAST(aisle AS UNSIGNED), aisle"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':warehouse' => $warehouse_id, ':zone' => $zone]);
|
||||
return array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'aisle');
|
||||
}
|
||||
|
||||
/**
|
||||
* All distinct racks in an aisle — for admin/read-only listing.
|
||||
*/
|
||||
public function getRacksAll(int $warehouse_id, string $zone, string $aisle): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT rack FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone AND aisle = :aisle
|
||||
ORDER BY CAST(rack AS UNSIGNED), rack"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':warehouse' => $warehouse_id, ':zone' => $zone, ':aisle' => $aisle]);
|
||||
return array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'rack');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ContactManager.php';
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_contact
|
||||
WHERE
|
||||
company_id = :company_id ";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$contact = new ContactManager($pdo2, $company_id);
|
||||
$answer['output'] = $contact->getContactList();
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,20 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ContactManager.php';
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_contact_type
|
||||
WHERE
|
||||
company_id = :company_id ";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$contact = new ContactManager($pdo2, $company_id);
|
||||
$answer['output'] = $contact->getContactTypeList();
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -2,105 +2,48 @@
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/FileUploader.php';
|
||||
require '../../../assets/utils/classes/ContactManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
try {
|
||||
/** 1. FILE HANDLING **/
|
||||
$uploader = new FileUploader($include_url . "uploads/contact/");
|
||||
$uploader = new FileUploader($include_url . 'uploads/contact/');
|
||||
|
||||
// Cleanup deleted files
|
||||
if ($id > 0) {
|
||||
$sql = "SELECT `contact_image` FROM md_contact WHERE company_id = :company_id AND id = :id";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([":company_id" => $company_id, ":id" => $id]);
|
||||
$sth->execute([':company_id' => $company_id, ':id' => $id]);
|
||||
$uploader->cleanup($sth->fetchColumn(), $data['keep_files'] ?? '');
|
||||
}
|
||||
|
||||
// Upload new files
|
||||
$errors = $uploader->upload('contact_image');
|
||||
if (!empty($errors)) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $errors[0];
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $errors[0];
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$db_image_string = $uploader->buildFileString($data['keep_files'] ?? '');
|
||||
|
||||
/** 2. DATABASE **/
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $id, $db_image_string) {
|
||||
|
||||
$sql = "SELECT `log` FROM md_contact
|
||||
WHERE company_id = :company_id AND id = :id";
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true);
|
||||
if (!is_array($table_log)) $table_log = [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
":company_id" => $company_id,
|
||||
":contact_name" => $data["contact_name"],
|
||||
":tax_id" => $data["tax_id"],
|
||||
":organization" => $data["organization"],
|
||||
":branch" => $data["branch"],
|
||||
":contact_type" => (int)$data["contact_type"],
|
||||
":billing_address" => $data["billing_address"],
|
||||
":shipping_location"=> $data["shipping_location"],
|
||||
":shipping_address" => $data["shipping_address"],
|
||||
":remark" => $data["remark"],
|
||||
":contact_image" => $db_image_string,
|
||||
":status" => (int)$data["status"],
|
||||
":log" => json_encode($table_log),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$sql = "UPDATE md_contact SET
|
||||
`contact_name` = :contact_name,
|
||||
`tax_id` = :tax_id,
|
||||
`organization` = :organization,
|
||||
`branch` = :branch,
|
||||
`contact_type` = :contact_type,
|
||||
`billing_address` = :billing_address,
|
||||
`shipping_location` = :shipping_location,
|
||||
`shipping_address` = :shipping_address,
|
||||
`remark` = :remark,
|
||||
`contact_image` = :contact_image,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
$params[":id"] = $id;
|
||||
} else {
|
||||
$sql = "INSERT INTO md_contact
|
||||
(company_id, contact_name, tax_id, organization, branch, contact_type,
|
||||
billing_address, shipping_location, shipping_address, remark, contact_image, status, log)
|
||||
VALUES
|
||||
(:company_id, :contact_name, :tax_id, :organization, :branch, :contact_type,
|
||||
:billing_address, :shipping_location, :shipping_address, :remark, :contact_image, :status, :log)";
|
||||
}
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute($params);
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $db_image_string) {
|
||||
$contact = new ContactManager($pdo, $company_id);
|
||||
$contact->saveContact($data, $logging, $db_image_string);
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// DB failed — rollback uploaded files
|
||||
if (isset($uploader)) $uploader->rollbackUploads();
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Database error, please try again';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($uploader)) $uploader->rollbackUploads();
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +1,24 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ContactManager.php';
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) {
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
$sql = "SELECT `log` FROM md_contact_type
|
||||
WHERE company_id = :company_id AND id = :id";
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true);
|
||||
if (!is_array($table_log)) $table_log = [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
":company_id" => $company_id,
|
||||
":contact_type" => $data["contact_type"],
|
||||
":description" => $data["description"],
|
||||
":status" => (int)$data["status"],
|
||||
":log" => json_encode($table_log),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$sql = "UPDATE md_contact_type SET
|
||||
`contact_type` = :contact_type,
|
||||
`description` = :description,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
|
||||
$params[":id"] = $id;
|
||||
|
||||
} else {
|
||||
|
||||
$sql = "INSERT INTO md_contact_type
|
||||
(company_id, contact_type, `description`, `status`, `log`)
|
||||
VALUES
|
||||
(:company_id, :contact_type, :description, :status, :log)";
|
||||
}
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute($params);
|
||||
|
||||
$contact = new ContactManager($pdo, $company_id);
|
||||
$contact->saveContactType($data, $logging);
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Database error, please try again';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,11 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ContactManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_contact
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$target_dir = $include_url."uploads/contact/";
|
||||
|
||||
$answer["contact_dir"] = $target_dir;
|
||||
$answer["output"] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$contact = new ContactManager($pdo2, $company_id);
|
||||
$answer['contact_dir'] = $include_url . 'uploads/contact/';
|
||||
$answer['output'] = $contact->getContactById((int)$data['id']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,25 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ContactManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_contact_type
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$contact = new ContactManager($pdo2, $company_id);
|
||||
$answer['output'] = $contact->getContactTypeById((int)$data['id']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,60 +1,13 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$product_sku = $data['product_sku'] ?? '';
|
||||
$lot_number = $data['lot_number'] ?? '';
|
||||
|
||||
if (!$product_sku || !$lot_number) {
|
||||
$answer['output'] = [];
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
// Discover all warehouses
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT id, warehouse_name FROM md_warehouse
|
||||
WHERE company_id = :company_id AND status = 1"
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$answer['output'] = $report->getLotStockLog(
|
||||
$data['product_sku'] ?? '',
|
||||
$data['lot_number'] ?? ''
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id]);
|
||||
$warehouses = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$rows = [];
|
||||
foreach ($warehouses as $wh) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
|
||||
$table = "td_stock_{$safe}";
|
||||
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT
|
||||
s.id,
|
||||
s.date,
|
||||
s.type,
|
||||
s.zone,
|
||||
s.aisle,
|
||||
s.rack,
|
||||
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
|
||||
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
|
||||
s.serial_number,
|
||||
s.description,
|
||||
'{$wh['warehouse_name']}' AS warehouse_name
|
||||
FROM `{$table}` s
|
||||
WHERE s.company_id = :company_id
|
||||
AND s.product_sku = :product_sku
|
||||
AND s.lot_number = :lot_number
|
||||
ORDER BY s.date DESC"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $company_id,
|
||||
':product_sku' => $product_sku,
|
||||
':lot_number' => $lot_number,
|
||||
]);
|
||||
$rows = array_merge($rows, $sth->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
// Sort all rows by date DESC
|
||||
usort($rows, fn($a, $b) => strcmp($b['date'], $a['date']));
|
||||
|
||||
$answer['output'] = $rows;
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -1,61 +1,16 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
// All lots with running balance across all warehouses
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT
|
||||
l.id,
|
||||
l.product_sku,
|
||||
l.lot_number,
|
||||
l.expiry_date,
|
||||
l.description,
|
||||
p.product_name,
|
||||
DATEDIFF(l.expiry_date, CURDATE()) AS days_remaining,
|
||||
COALESCE(
|
||||
(SELECT ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2)
|
||||
FROM warehouse_balance wb
|
||||
WHERE wb.company_id = l.company_id
|
||||
AND wb.product_sku = l.product_sku),
|
||||
0
|
||||
) AS balance
|
||||
FROM md_lot l
|
||||
INNER JOIN md_product p
|
||||
ON p.company_id = l.company_id
|
||||
AND p.sku = l.product_sku
|
||||
WHERE l.company_id = :company_id
|
||||
ORDER BY l.product_sku, l.expiry_date ASC"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id]);
|
||||
db_check($sth, $answer);
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$result = $report->getProductLots();
|
||||
|
||||
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$total = count($rows);
|
||||
$active = 0;
|
||||
$expired = 0;
|
||||
$near = 0;
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$days = (int) $row['days_remaining'];
|
||||
$balance = (float) $row['balance'];
|
||||
|
||||
if ($balance > 0) $active++;
|
||||
if ($days < 0) $expired++;
|
||||
if ($days >= 0 && $days <= 30) $near++;
|
||||
|
||||
// Status
|
||||
if ($days < 0) $row['status'] = 'expired';
|
||||
elseif ($days <= 7) $row['status'] = 'critical';
|
||||
elseif ($days <= 30) $row['status'] = 'near';
|
||||
else $row['status'] = 'ok';
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$answer['output'] = $rows;
|
||||
$answer['total'] = $total;
|
||||
$answer['active'] = $active;
|
||||
$answer['expired'] = $expired;
|
||||
$answer['near'] = $near;
|
||||
$answer['output'] = $result['rows'];
|
||||
$answer['total'] = $result['total'];
|
||||
$answer['active'] = $result['active'];
|
||||
$answer['expired'] = $result['expired'];
|
||||
$answer['near'] = $result['near'];
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$rack_id = (int)($data['rack_id'] ?? 0);
|
||||
|
||||
@@ -10,29 +11,8 @@
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$sql = "SELECT
|
||||
rl.id,
|
||||
rl.dt,
|
||||
rl.action,
|
||||
rl.product_sku,
|
||||
rl.td_stock_id,
|
||||
rl.login,
|
||||
u.name AS user_name
|
||||
FROM md_rack_log rl
|
||||
LEFT JOIN {$db_database}.user u
|
||||
ON u.user_id = rl.user_id
|
||||
WHERE rl.company_id = :company_id
|
||||
AND rl.md_rack_id = :rack_id
|
||||
ORDER BY rl.dt DESC";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
':company_id' => $company_id,
|
||||
':rack_id' => $rack_id,
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$answer['output'] = $report->getRackLog($rack_id);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -4,42 +4,13 @@
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
|
||||
// Stats
|
||||
$total = $report->getTotalCapacity();
|
||||
$occupied = $report->getSpaceUsed();
|
||||
$empty = $total - $occupied;
|
||||
|
||||
// All racks with warehouse name, product info and stock info
|
||||
$sql = "SELECT
|
||||
r.id,
|
||||
r.zone,
|
||||
r.aisle,
|
||||
r.rack,
|
||||
r.product_sku,
|
||||
r.td_stock_id,
|
||||
mw.warehouse_name,
|
||||
mw.id AS warehouse_id,
|
||||
p.product_name,
|
||||
CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status
|
||||
FROM md_rack r
|
||||
INNER JOIN md_warehouse mw
|
||||
ON mw.company_id = r.company_id
|
||||
AND mw.id = r.warehouse
|
||||
LEFT JOIN md_product p
|
||||
ON p.company_id = r.company_id
|
||||
AND p.sku = r.product_sku
|
||||
WHERE r.company_id = :company_id
|
||||
ORDER BY mw.warehouse_name, r.zone, CAST(r.aisle AS UNSIGNED), r.aisle, CAST(r.rack AS UNSIGNED), r.rack";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([':company_id' => $company_id]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer['output'] = $report->getRackOccupancy();
|
||||
$answer['total'] = $total;
|
||||
$answer['occupied'] = $occupied;
|
||||
$answer['empty'] = $empty;
|
||||
$answer['empty'] = $total - $occupied;
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -1,25 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ContactManager.php';
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_contact
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
contact_name like :keyword
|
||||
LIMIT 50
|
||||
";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":keyword" => '%'.$data["keyword"].'%'
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$result = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["result"] = $result;
|
||||
$answer["success"] = 1;
|
||||
$contact = new ContactManager($pdo2, $company_id);
|
||||
$answer['result'] = $contact->searchContact($data['keyword'] ?? '');
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,122 +1,25 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/StockManager.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, &$answer) {
|
||||
|
||||
$id = (int)$data['id'];
|
||||
$warehouse_id = (int)$data["warehouse"];
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
|
||||
$warehouse = $whMgmt->getStockContext($warehouse_id, $id);
|
||||
$row = $warehouse['row'] ?? [];
|
||||
$table = $warehouse['table'];
|
||||
|
||||
// Append this edit/create event to td_stock_<wh>.log (stock record history)
|
||||
$table_log = json_decode($row['log'] ?? '[]', true);
|
||||
$table_log[] = $logging;
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
// UPDATE existing stock_in record
|
||||
$sql = "UPDATE `$table` SET
|
||||
`contact_id` = :contact_id,
|
||||
`description` = :description,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":id" => $id,
|
||||
":company_id" => $company_id,
|
||||
":contact_id" => (int)$data["contact_id"],
|
||||
":description" => $data["description"],
|
||||
":log" => json_encode($table_log),
|
||||
]);
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
// Upsert md_lot if lot_number + expiry_date provided
|
||||
$lot_number = $data["lot_number"] ?: null;
|
||||
$expiry_date = $data["expiry_date"] ?: null;
|
||||
|
||||
if ($lot_number && $expiry_date) {
|
||||
$sth = $pdo->prepare("
|
||||
INSERT INTO md_lot (company_id, product_sku, lot_number, expiry_date)
|
||||
VALUES (:company_id, :product_sku, :lot_number, :expiry_date)
|
||||
ON DUPLICATE KEY UPDATE expiry_date = expiry_date
|
||||
");
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":product_sku" => $data["product_sku"],
|
||||
":lot_number" => $lot_number,
|
||||
":expiry_date" => $expiry_date,
|
||||
]);
|
||||
}
|
||||
|
||||
// INSERT new stock_in record
|
||||
$sql = "INSERT INTO `$table`
|
||||
(uuid, company_id, `date`, product_sku, `in`, zone, aisle, rack, contact_id, `description`, `log`, `type`, `lot_number`, `serial_number`)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack, :contact_id, :description, :log, 'in', :lot_number, :serial_number)";
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":uuid" => $uuid,
|
||||
":company_id" => $company_id,
|
||||
":date" => date('Y-m-d H:i:s'),
|
||||
":product_sku" => $data["product_sku"],
|
||||
":quantity" => $data["quantity"],
|
||||
":zone" => $data["zone"],
|
||||
":aisle" => $data["aisle"],
|
||||
":rack" => $data["rack"],
|
||||
":contact_id" => (int)$data["contact_id"],
|
||||
":description" => $data["description"],
|
||||
":log" => json_encode($table_log),
|
||||
":lot_number" => $lot_number,
|
||||
":serial_number" => $data["serial_number"] ?: null,
|
||||
]);
|
||||
|
||||
// Grab the newly-generated ID to link the rack
|
||||
$td_stock_id = (int) $pdo->lastInsertId();
|
||||
|
||||
// Link the rack to this new stock record
|
||||
$whMgmt->occupyRack(
|
||||
$warehouse_id,
|
||||
$data['zone'], $data['aisle'], $data['rack'],
|
||||
$data['product_sku'],
|
||||
$td_stock_id
|
||||
);
|
||||
// Balance adjustment handles both INSERT (delta = full qty)
|
||||
// and UPDATE (delta = new - old) via the same formula.
|
||||
$whMgmt->adjustBalance(
|
||||
'in',
|
||||
$warehouse_id,
|
||||
$data["product_sku"],
|
||||
$row['in'] ?? 0,
|
||||
$data["quantity"]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid) {
|
||||
$stock = new StockManager($pdo, $company_id);
|
||||
$stock->saveStockIn($data, $logging, $uuid);
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Database error, please try again';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,145 +1,25 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/StockManager.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid) {
|
||||
|
||||
$id = (int)$data['id'];
|
||||
$warehouse_id = (int)$data["warehouse"];
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
|
||||
$warehouse = $whMgmt->getStockContext($warehouse_id, $id);
|
||||
$row = $warehouse['row'] ?? [];
|
||||
$table = $warehouse['table'];
|
||||
|
||||
// Append this edit/create event to td_stock_<wh>.log (stock record history)
|
||||
$table_log = json_decode($row['log'] ?? '[]', true);
|
||||
$table_log[] = $logging;
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
// UPDATE existing stock_out record
|
||||
$sql = "UPDATE `$table` SET
|
||||
`contact_id` = :contact_id,
|
||||
`description` = :description,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":id" => $id,
|
||||
":company_id" => $company_id,
|
||||
":contact_id" => (int)$data["contact_id"],
|
||||
":description" => $data["description"],
|
||||
":log" => json_encode($table_log),
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
||||
// Look up the source stock record via the rack.
|
||||
// Under 1:1, the rack points to exactly one td_stock_<wh> row.
|
||||
$source_stock = $whMgmt->getRackStock(
|
||||
$warehouse_id,
|
||||
$data["zone"],
|
||||
$data["aisle"],
|
||||
$data["rack"]
|
||||
);
|
||||
|
||||
if (!$source_stock) {
|
||||
throw new Exception(
|
||||
"Rack {$data['zone']}-{$data['aisle']}-{$data['rack']} is empty — nothing to take out."
|
||||
);
|
||||
}
|
||||
|
||||
// Defensive checks — UI cascade should already prevent mismatches
|
||||
if ($source_stock['product_sku'] !== $data["product_sku"]) {
|
||||
throw new Exception(
|
||||
"Rack holds {$source_stock['product_sku']}, not {$data['product_sku']}."
|
||||
);
|
||||
}
|
||||
|
||||
// If user selected a specific lot, verify it matches the rack's stock
|
||||
if (!empty($data["lot_number"]) && $source_stock['lot_number'] !== $data["lot_number"]) {
|
||||
throw new Exception(
|
||||
"Rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
// If user selected a specific serial, verify it matches the rack's stock
|
||||
if (!empty($data["serial_number"]) && $source_stock['serial_number'] !== $data["serial_number"]) {
|
||||
throw new Exception(
|
||||
"Rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
// Quantity comes from the source stock_in row, NOT user input.
|
||||
// Under 1:1, taking from this rack = taking the full batch.
|
||||
$quantity = (int) $source_stock['in'];
|
||||
$ref_id = (int) $source_stock['id'];
|
||||
$lot_number = $source_stock['lot_number'] ?? null;
|
||||
$serial_number = $source_stock['serial_number'] ?? null;
|
||||
|
||||
// INSERT new stock_out record.
|
||||
// lot_number and serial_number carried from source stock_in row.
|
||||
// ref_id links this stock_out row back to the stock_in batch it consumed.
|
||||
$sql = "INSERT INTO `$table`
|
||||
(uuid, company_id, `date`, product_sku, `out`, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, ref_id, lot_number, serial_number)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number)";
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":uuid" => $uuid,
|
||||
":company_id" => $company_id,
|
||||
":date" => date('Y-m-d H:i:s'),
|
||||
":product_sku" => $data["product_sku"],
|
||||
":quantity" => $quantity,
|
||||
":zone" => $data["zone"],
|
||||
":aisle" => $data["aisle"],
|
||||
":rack" => $data["rack"],
|
||||
":contact_id" => (int)$data["contact_id"],
|
||||
":description" => $data["description"],
|
||||
":log" => json_encode($table_log),
|
||||
":ref_id" => $ref_id,
|
||||
":lot_number" => $lot_number,
|
||||
":serial_number" => $serial_number,
|
||||
]);
|
||||
|
||||
// Release the rack — stock is now gone from this location.
|
||||
$whMgmt->releaseRack(
|
||||
$warehouse_id,
|
||||
$data["zone"], $data["aisle"], $data["rack"]
|
||||
);
|
||||
// Balance: total_out increases by delta.
|
||||
// INSERT: $row['out'] = 0 → delta = full quantity
|
||||
// UPDATE: $row['out'] = old → delta = new - old
|
||||
$whMgmt->adjustBalance(
|
||||
'out',
|
||||
$warehouse_id,
|
||||
$data["product_sku"],
|
||||
$row['out'] ?? 0,
|
||||
$quantity
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
$stock = new StockManager($pdo, $company_id);
|
||||
$stock->saveStockOut($data, $logging, $uuid);
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Database error, please try again';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,239 +1,25 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/StockManager.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid) {
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
|
||||
// Metadata that's editable on BOTH create and edit
|
||||
$contact_id = (int)($data['contact_id'] ?? 0);
|
||||
$description = ($data['description'] ?? '');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// EDIT FLOW
|
||||
// Physical state (racks, balances, quantities) is frozen.
|
||||
// Only metadata (contact, description) can change.
|
||||
// ══════════════════════════════════════════════════════════
|
||||
if ($id > 0) {
|
||||
|
||||
// Find the "from" row that's being edited.
|
||||
// We look it up across all td_stock_<wh> via warehouse_from
|
||||
// since $id alone isn't enough (it's per-table).
|
||||
$from_warehouse = (int)($data['warehouse_from'] ?? 0);
|
||||
$to_warehouse = (int)($data['warehouse_to'] ?? 0);
|
||||
|
||||
$from_ctx = $whMgmt->getStockContext($from_warehouse, $id);
|
||||
$from_table = $from_ctx['table'];
|
||||
$from_row = $from_ctx['row'];
|
||||
|
||||
if (!$from_row) {
|
||||
throw new Exception("Transfer record not found.");
|
||||
}
|
||||
|
||||
// Find the paired "to" row via ref_id stored on the "from" row
|
||||
$to_id = (int) $from_row['ref_id'];
|
||||
$to_ctx = $whMgmt->getStockContext($to_warehouse, $to_id);
|
||||
$to_table = $to_ctx['table'];
|
||||
$to_row = $to_ctx['row'];
|
||||
|
||||
if (!$to_row) {
|
||||
throw new Exception("Paired destination record missing — data integrity issue.");
|
||||
}
|
||||
|
||||
// Append this edit to both rows' logs
|
||||
$from_log = json_decode($from_row['log'] ?? '[]', true) ?: [];
|
||||
$from_log[] = $logging;
|
||||
|
||||
$to_log = json_decode($to_row['log'] ?? '[]', true) ?: [];
|
||||
$to_log[] = $logging;
|
||||
|
||||
// UPDATE "from" row — only metadata fields
|
||||
$sql = "UPDATE `$from_table` SET
|
||||
contact_id = :contact_id,
|
||||
description = :description,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
$pdo->prepare($sql)->execute([
|
||||
":contact_id" => $contact_id,
|
||||
":description" => $description,
|
||||
":log" => json_encode($from_log),
|
||||
":id" => $id,
|
||||
":company_id" => $company_id,
|
||||
]);
|
||||
|
||||
// UPDATE "to" row — same metadata
|
||||
$sql = "UPDATE `$to_table` SET
|
||||
contact_id = :contact_id,
|
||||
description = :description,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
$pdo->prepare($sql)->execute([
|
||||
":contact_id" => $contact_id,
|
||||
":description" => $description,
|
||||
":log" => json_encode($to_log),
|
||||
":id" => $to_id,
|
||||
":company_id" => $company_id,
|
||||
]);
|
||||
|
||||
// Done — no rack ops, no balance changes on edit.
|
||||
return;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// CREATE FLOW (unchanged from before)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
// Gather source + destination parameters
|
||||
$from_warehouse = (int)$data['warehouse_from'];
|
||||
$from_zone = $data['zone_from'];
|
||||
$from_aisle = $data['aisle_from'];
|
||||
$from_rack = $data['rack_from'];
|
||||
|
||||
$to_warehouse = (int)$data['warehouse_to'];
|
||||
$to_zone = $data['zone_to'];
|
||||
$to_aisle = $data['aisle_to'];
|
||||
$to_rack = $data['rack_to'];
|
||||
|
||||
// Validate source rack has stock of the expected SKU.
|
||||
// Quantity comes from here — NOT user input.
|
||||
$source_stock = $whMgmt->getRackStock(
|
||||
$from_warehouse, $from_zone, $from_aisle, $from_rack
|
||||
);
|
||||
|
||||
if (!$source_stock) {
|
||||
throw new Exception(
|
||||
"Source rack {$from_zone}-{$from_aisle}-{$from_rack} is empty."
|
||||
);
|
||||
}
|
||||
|
||||
if ($source_stock['product_sku'] !== $data["product_sku"]) {
|
||||
throw new Exception(
|
||||
"Source rack holds {$source_stock['product_sku']}, not {$data['product_sku']}."
|
||||
);
|
||||
}
|
||||
|
||||
// Defensive lot/serial checks
|
||||
if (!empty($data["lot_number"]) && $source_stock['lot_number'] !== $data["lot_number"]) {
|
||||
throw new Exception(
|
||||
"Source rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
|
||||
);
|
||||
}
|
||||
if (!empty($data["serial_number"]) && $source_stock['serial_number'] !== $data["serial_number"]) {
|
||||
throw new Exception(
|
||||
"Source rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
$quantity = (int) $source_stock['in'];
|
||||
$lot_number = $source_stock['lot_number'] ?? null;
|
||||
$serial_number = $source_stock['serial_number'] ?? null;
|
||||
|
||||
// Resolve both td_stock_<wh> tables
|
||||
$from_ctx = $whMgmt->getStockContext($from_warehouse, 0);
|
||||
$to_ctx = $whMgmt->getStockContext($to_warehouse, 0);
|
||||
$from_table = $from_ctx['table'];
|
||||
$to_table = $to_ctx['table'];
|
||||
|
||||
// Share UUID across both paired rows for easy grouping
|
||||
$transfer_uuid = $uuid;
|
||||
|
||||
// INSERT "from" row (outbound from source warehouse)
|
||||
$table_log = [$logging];
|
||||
|
||||
$sql = "INSERT INTO `$from_table`
|
||||
(uuid, company_id, `date`, product_sku, `out`,
|
||||
ref_warehouse, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:ref_warehouse, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number)";
|
||||
$pdo->prepare($sql)->execute([
|
||||
":uuid" => $transfer_uuid,
|
||||
":company_id" => $company_id,
|
||||
":date" => date('Y-m-d H:i:s'),
|
||||
":product_sku" => $data["product_sku"],
|
||||
":quantity" => $quantity,
|
||||
":ref_warehouse" => $to_warehouse,
|
||||
":zone" => $from_zone,
|
||||
":aisle" => $from_aisle,
|
||||
":rack" => $from_rack,
|
||||
":contact_id" => $contact_id,
|
||||
":description" => $description,
|
||||
":log" => json_encode($table_log),
|
||||
":lot_number" => $lot_number,
|
||||
":serial_number" => $serial_number,
|
||||
]);
|
||||
$from_stock_id = (int) $pdo->lastInsertId();
|
||||
|
||||
// INSERT "to" row (inbound at destination warehouse)
|
||||
$sql = "INSERT INTO `$to_table`
|
||||
(uuid, company_id, `date`, product_sku, `in`,
|
||||
ref_warehouse, ref_id, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:ref_warehouse, :ref_id, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number)";
|
||||
$pdo->prepare($sql)->execute([
|
||||
":uuid" => $transfer_uuid,
|
||||
":company_id" => $company_id,
|
||||
":date" => date('Y-m-d H:i:s'),
|
||||
":product_sku" => $data["product_sku"],
|
||||
":quantity" => $quantity,
|
||||
":ref_warehouse" => $from_warehouse,
|
||||
":ref_id" => $from_stock_id,
|
||||
":zone" => $to_zone,
|
||||
":aisle" => $to_aisle,
|
||||
":rack" => $to_rack,
|
||||
":contact_id" => $contact_id,
|
||||
":description" => $description,
|
||||
":log" => json_encode($table_log),
|
||||
":lot_number" => $lot_number,
|
||||
":serial_number" => $serial_number,
|
||||
]);
|
||||
$to_stock_id = (int) $pdo->lastInsertId();
|
||||
|
||||
// Back-fill the "from" row's ref_id now that we have to_stock_id
|
||||
// so both rows point at each other
|
||||
$sql = "UPDATE `$from_table`
|
||||
SET ref_id = :ref_id
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
$pdo->prepare($sql)->execute([
|
||||
":ref_id" => $to_stock_id,
|
||||
":id" => $from_stock_id,
|
||||
":company_id" => $company_id,
|
||||
]);
|
||||
|
||||
// Move rack state: release source, occupy destination
|
||||
// md_rack.log gets 'release' + 'occupy' entries automatically
|
||||
$whMgmt->releaseRack($from_warehouse, $from_zone, $from_aisle, $from_rack);
|
||||
$whMgmt->occupyRack(
|
||||
$to_warehouse, $to_zone, $to_aisle, $to_rack,
|
||||
$data["product_sku"], $to_stock_id
|
||||
);
|
||||
|
||||
// Balance adjustments on both warehouses
|
||||
$whMgmt->adjustBalance('out', $from_warehouse, $data["product_sku"], 0, $quantity);
|
||||
$whMgmt->adjustBalance('in', $to_warehouse, $data["product_sku"], 0, $quantity);
|
||||
|
||||
$stock = new StockManager($pdo, $company_id);
|
||||
$stock->saveStockTransfer($data, $logging, $uuid);
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Database error, please try again';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_product
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
sku like :keyword
|
||||
LIMIT 50
|
||||
";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":keyword" => '%'.$data["keyword"].'%'
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$result = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["result"] = $result;
|
||||
$answer["success"] = 1;
|
||||
$product = new ProductManager($pdo2, $company_id);
|
||||
$answer['result'] = $product->searchProduct($data['keyword'] ?? '');
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -3,8 +3,6 @@
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
// Returns active lots for a given product_sku
|
||||
// "Active" = EXISTS in an occupied md_rack (product_sku IS NOT NULL)
|
||||
$product_sku = $data['product_sku'] ?? '';
|
||||
|
||||
if (empty($product_sku)) {
|
||||
@@ -13,52 +11,8 @@
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
// Discover all warehouses
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT id, warehouse_name FROM md_warehouse
|
||||
WHERE company_id = :company_id AND status = 1"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id]);
|
||||
$warehouses = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$active_lots = [];
|
||||
|
||||
foreach ($warehouses as $wh) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
|
||||
$table = "td_stock_{$safe}";
|
||||
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT DISTINCT s.lot_number, l.expiry_date
|
||||
FROM `{$table}` s
|
||||
LEFT JOIN md_lot l
|
||||
ON l.company_id = s.company_id
|
||||
AND l.product_sku = s.product_sku
|
||||
AND l.lot_number = s.lot_number
|
||||
WHERE s.company_id = :company_id
|
||||
AND s.product_sku = :product_sku
|
||||
AND s.type = 'in'
|
||||
AND s.lot_number IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM md_rack r
|
||||
WHERE r.company_id = s.company_id
|
||||
AND r.td_stock_id = s.id
|
||||
AND r.product_sku IS NOT NULL
|
||||
)"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $company_id,
|
||||
':product_sku' => $product_sku,
|
||||
]);
|
||||
|
||||
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$key = $row['lot_number'];
|
||||
if (!isset($active_lots[$key])) {
|
||||
$active_lots[$key] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$answer['output'] = array_values($active_lots);
|
||||
$wh = new WarehouseManager($pdo2, $company_id);
|
||||
$answer['output'] = $wh->getActiveLots($product_sku);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -1,11 +1,9 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
// Returns active serials for a given product_sku + optional lot_number
|
||||
// "Active" = EXISTS in an occupied md_rack
|
||||
$product_sku = $data['product_sku'] ?? '';
|
||||
$lot_number = $data['lot_number'] ?? '';
|
||||
|
||||
if (empty($product_sku)) {
|
||||
$answer['output'] = [];
|
||||
@@ -13,52 +11,8 @@
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
// Discover all warehouses
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT id, warehouse_name FROM md_warehouse
|
||||
WHERE company_id = :company_id AND status = 1"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id]);
|
||||
$warehouses = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$active_serials = [];
|
||||
|
||||
foreach ($warehouses as $wh) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
|
||||
$table = "td_stock_{$safe}";
|
||||
|
||||
$sql = "SELECT DISTINCT s.serial_number
|
||||
FROM `{$table}` s
|
||||
WHERE s.company_id = :company_id
|
||||
AND s.product_sku = :product_sku
|
||||
AND s.type = 'in'
|
||||
AND s.serial_number IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM md_rack r
|
||||
WHERE r.company_id = s.company_id
|
||||
AND r.td_stock_id = s.id
|
||||
AND r.product_sku IS NOT NULL
|
||||
)";
|
||||
|
||||
$params = [
|
||||
':company_id' => $company_id,
|
||||
':product_sku' => $product_sku,
|
||||
];
|
||||
|
||||
if (!empty($lot_number)) {
|
||||
$sql .= " AND s.lot_number = :lot_number";
|
||||
$params[':lot_number'] = $lot_number;
|
||||
}
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute($params);
|
||||
|
||||
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$active_serials[$row['serial_number']] = $row['serial_number'];
|
||||
}
|
||||
}
|
||||
|
||||
$answer['output'] = array_values($active_serials);
|
||||
$wh = new WarehouseManager($pdo2, $company_id);
|
||||
$answer['output'] = $wh->getActiveSerials($product_sku, $data['lot_number'] ?? '');
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -6,15 +6,15 @@
|
||||
$type = $data['type'] ?? '';
|
||||
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||
|
||||
// Resolve warehouse and zone
|
||||
$warehouse = (int)($data['warehouse'] ?? 0);
|
||||
$zone = $data['zone'] ?? '';
|
||||
if (isset($data['warehouse_from']) && $type === 'from') { $warehouse = (int)$data['warehouse_from']; $zone = $data['zone_from'] ?? $zone; }
|
||||
if (isset($data['warehouse_to']) && $type === 'to') { $warehouse = (int)$data['warehouse_to']; $zone = $data['zone_to'] ?? $zone; }
|
||||
|
||||
if ($type === 'to') {
|
||||
if (!empty($data['id'])) {
|
||||
$output = $whMgmt->getAislesAll($warehouse, $zone);
|
||||
} elseif ($type === 'to') {
|
||||
$output = $whMgmt->getAislesIn($warehouse, $zone);
|
||||
|
||||
} elseif ($type === 'from') {
|
||||
$output = $whMgmt->getAislesOut(
|
||||
$warehouse,
|
||||
@@ -23,26 +23,8 @@
|
||||
$data['lot_number'] ?? null,
|
||||
$data['serial_number'] ?? null
|
||||
);
|
||||
|
||||
} else {
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT DISTINCT aisle FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone
|
||||
ORDER BY CAST(aisle AS UNSIGNED), aisle"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse, ':zone' => $zone]);
|
||||
$output = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'aisle');
|
||||
}
|
||||
|
||||
// Edit mode
|
||||
if (!empty($data['id'])) {
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT DISTINCT aisle FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone
|
||||
ORDER BY CAST(aisle AS UNSIGNED), aisle"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse, ':zone' => $zone]);
|
||||
$output = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'aisle');
|
||||
$output = $whMgmt->getAislesAll($warehouse, $zone);
|
||||
}
|
||||
|
||||
$answer['output'] = $output;
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_lot
|
||||
WHERE
|
||||
company_id = :company_id AND
|
||||
product_sku = :product_sku AND
|
||||
lot_number LIKE :keyword
|
||||
ORDER BY lot_number ASC
|
||||
LIMIT 50";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":product_sku" => $data["product_sku"] ?? '',
|
||||
":keyword" => '%'.$data["keyword"].'%'
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["result"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$wh = new WarehouseManager($pdo2, $company_id);
|
||||
$answer['result'] = $wh->getLotList(
|
||||
$data['product_sku'] ?? '',
|
||||
$data['keyword'] ?? ''
|
||||
);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -6,16 +6,16 @@
|
||||
$type = $data['type'] ?? '';
|
||||
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||
|
||||
// Resolve warehouse, zone and aisle
|
||||
$warehouse = (int)($data['warehouse'] ?? 0);
|
||||
$zone = $data['zone'] ?? '';
|
||||
$aisle = $data['aisle'] ?? '';
|
||||
if (isset($data['warehouse_from']) && $type === 'from') { $warehouse = (int)$data['warehouse_from']; $zone = $data['zone_from'] ?? $zone; $aisle = $data['aisle_from'] ?? $aisle; }
|
||||
if (isset($data['warehouse_to']) && $type === 'to') { $warehouse = (int)$data['warehouse_to']; $zone = $data['zone_to'] ?? $zone; $aisle = $data['aisle_to'] ?? $aisle; }
|
||||
|
||||
if ($type === 'to') {
|
||||
if (!empty($data['id'])) {
|
||||
$output = $whMgmt->getRacksAll($warehouse, $zone, $aisle);
|
||||
} elseif ($type === 'to') {
|
||||
$output = $whMgmt->getRacksIn($warehouse, $zone, $aisle);
|
||||
|
||||
} elseif ($type === 'from') {
|
||||
$output = $whMgmt->getRacksOut(
|
||||
$warehouse,
|
||||
@@ -25,26 +25,8 @@
|
||||
$data['lot_number'] ?? null,
|
||||
$data['serial_number'] ?? null
|
||||
);
|
||||
|
||||
} else {
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT DISTINCT rack FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone AND aisle = :aisle
|
||||
ORDER BY CAST(rack AS UNSIGNED), rack"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse, ':zone' => $zone, ':aisle' => $aisle]);
|
||||
$output = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'rack');
|
||||
}
|
||||
|
||||
// Edit mode
|
||||
if (!empty($data['id'])) {
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT DISTINCT rack FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone AND aisle = :aisle
|
||||
ORDER BY CAST(rack AS UNSIGNED), rack"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse, ':zone' => $zone, ':aisle' => $aisle]);
|
||||
$output = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'rack');
|
||||
$output = $whMgmt->getRacksAll($warehouse, $zone, $aisle);
|
||||
}
|
||||
|
||||
$answer['output'] = $output;
|
||||
|
||||
@@ -1,55 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/StockManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
$warehouse = (int)$data["wh"];
|
||||
|
||||
// get warehouse name
|
||||
$sql = "SELECT warehouse_name
|
||||
FROM md_warehouse
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :warehouse
|
||||
";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":warehouse" => $warehouse
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$warehouse = $sth->fetchColumn();
|
||||
|
||||
$table = "td_stock_" . $warehouse;
|
||||
|
||||
$sql = "SELECT a.* , a.in as quantity, b.contact_name, c.product_name, d.expiry_date
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b ON
|
||||
a.company_id = b.company_id and
|
||||
a.contact_id = b.id
|
||||
LEFT JOIN md_product c ON
|
||||
a.company_id = c.company_id and
|
||||
a.product_sku = c.sku
|
||||
LEFT JOIN md_lot d ON
|
||||
a.company_id = d.company_id and
|
||||
a.product_sku = d.product_sku and
|
||||
a.lot_number = d.lot_number
|
||||
WHERE
|
||||
a.company_id = :company_id and
|
||||
a.id = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$stock = new StockManager($pdo2, $company_id);
|
||||
$answer['output'] = $stock->getStockInById((int)$data['wh'], (int)$data['id']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,51 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/StockManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
$warehouse = (int)$data["wh"];
|
||||
|
||||
// get warehouse name
|
||||
$sql = "SELECT warehouse_name
|
||||
FROM md_warehouse
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :warehouse
|
||||
";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":warehouse" => $warehouse
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$warehouse = $sth->fetchColumn();
|
||||
|
||||
$table = "td_stock_" . $warehouse;
|
||||
|
||||
$sql = "SELECT a.* , a.out as quantity, b.contact_name, c.product_name
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b ON
|
||||
a.company_id = b.company_id and
|
||||
a.contact_id = b.id
|
||||
LEFT JOIN md_product c ON
|
||||
a.company_id = c.company_id and
|
||||
a.product_sku = c.sku
|
||||
WHERE
|
||||
a.company_id = :company_id and
|
||||
a.id = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$stock = new StockManager($pdo2, $company_id);
|
||||
$answer['output'] = $stock->getStockOutById((int)$data['wh'], (int)$data['id']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,101 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/StockManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
$warehouse = (int)$data["wh"];
|
||||
|
||||
/**
|
||||
* FROM warehouse, zone, aisle, rack, product name, quantity, contact name
|
||||
*/
|
||||
|
||||
// get warehouse name
|
||||
$sql = "SELECT warehouse_name
|
||||
FROM md_warehouse
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :warehouse
|
||||
";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":warehouse" => $warehouse
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$warehouse = $sth->fetchColumn();
|
||||
|
||||
$table = "td_stock_" . $warehouse;
|
||||
|
||||
$sql = "SELECT a.* , a.out as quantity, b.contact_name, c.product_name
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b ON
|
||||
a.company_id = b.company_id and
|
||||
a.contact_id = b.id
|
||||
LEFT JOIN md_product c ON
|
||||
a.company_id = c.company_id and
|
||||
a.product_sku = c.sku
|
||||
WHERE
|
||||
a.company_id = :company_id and
|
||||
a.id = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$output = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
/**
|
||||
* TO warehouse, zone, aisle, rack, product name, quantity, contact name
|
||||
*/
|
||||
|
||||
// get warehouse name
|
||||
$sql = "SELECT warehouse_name
|
||||
FROM md_warehouse
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :warehouse
|
||||
";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":warehouse" => $output["ref_warehouse"]
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$warehouse = $sth->fetchColumn();
|
||||
|
||||
$table = "td_stock_" . $warehouse;
|
||||
|
||||
$sql = "SELECT a.* , a.in as quantity, b.contact_name, c.product_name
|
||||
FROM `{$table}` a
|
||||
LEFT JOIN md_contact b ON
|
||||
a.company_id = b.company_id and
|
||||
a.contact_id = b.id
|
||||
LEFT JOIN md_product c ON
|
||||
a.company_id = b.company_id and
|
||||
a.product_sku = c.sku
|
||||
WHERE
|
||||
a.company_id = :company_id and
|
||||
a.uuid = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $output["uuid"]
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$output["ref"] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$answer["output"] = $output;
|
||||
$answer["success"] = 1;
|
||||
$stock = new StockManager($pdo2, $company_id);
|
||||
$answer['output'] = $stock->getTransferById((int)$data['wh'], (int)$data['id']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,46 +1,14 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
// Build filter + params based on operation type.
|
||||
// Filter warehouses by occupancy:
|
||||
// 'to' = destination → only warehouses with at least one empty rack
|
||||
// 'from' = source → only warehouses that hold the specific product SKU
|
||||
// other = all warehouses (for read-only listings, dashboards, etc.)
|
||||
$product_sku_filter = "";
|
||||
$params = [":company_id" => $company_id];
|
||||
|
||||
if ($data["type"] == "to") {
|
||||
$product_sku_filter = "AND r.product_sku IS NULL";
|
||||
|
||||
} elseif ($data["type"] == "from") {
|
||||
$product_sku_filter = "AND r.product_sku = :product_sku";
|
||||
$params[":product_sku"] = $data["product_sku"];
|
||||
}
|
||||
|
||||
if(!empty($data["id"])){
|
||||
$product_sku_filter = "";
|
||||
unset($params[":product_sku"]);
|
||||
}
|
||||
|
||||
// Only list warehouses that have racks matching the occupancy filter.
|
||||
// EXISTS is efficient here — it stops searching as soon as one match is found.
|
||||
$sql = "SELECT w.id, w.warehouse_name
|
||||
FROM md_warehouse w
|
||||
WHERE w.company_id = :company_id
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM md_rack r
|
||||
WHERE r.company_id = w.company_id
|
||||
AND r.warehouse = w.id
|
||||
$product_sku_filter
|
||||
)
|
||||
ORDER BY w.warehouse_name";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute($params);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$wh = new WarehouseManager($pdo2, $company_id);
|
||||
$answer['output'] = $wh->getWarehouseList(
|
||||
$data['type'] ?? '',
|
||||
$data['product_sku'] ?? '',
|
||||
(int)($data['id'] ?? 0)
|
||||
);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -6,44 +6,23 @@
|
||||
$type = $data['type'] ?? '';
|
||||
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||
|
||||
// Resolve warehouse — supports warehouse, warehouse_from, warehouse_to
|
||||
$warehouse = (int)($data['warehouse'] ?? 0);
|
||||
if (isset($data['warehouse_from']) && $type === 'from') $warehouse = (int)$data['warehouse_from'];
|
||||
if (isset($data['warehouse_to']) && $type === 'to') $warehouse = (int)$data['warehouse_to'];
|
||||
|
||||
if ($type === 'to') {
|
||||
// Stock_in destination — empty racks only
|
||||
if (!empty($data['id'])) {
|
||||
$output = $whMgmt->getZonesAll($warehouse);
|
||||
} elseif ($type === 'to') {
|
||||
$output = $whMgmt->getZonesIn($warehouse);
|
||||
|
||||
} elseif ($type === 'from') {
|
||||
// Stock_out source — filter by sku + lot + serial
|
||||
$output = $whMgmt->getZonesOut(
|
||||
$warehouse,
|
||||
$data['product_sku'] ?? '',
|
||||
$data['lot_number'] ?? null,
|
||||
$data['serial_number'] ?? null
|
||||
);
|
||||
|
||||
} else {
|
||||
// Admin / read-only — all zones in the warehouse
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT DISTINCT zone FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse
|
||||
ORDER BY CAST(zone AS UNSIGNED), zone"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse]);
|
||||
$output = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// Edit mode — return all zones regardless of occupancy
|
||||
if (!empty($data['id'])) {
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT DISTINCT zone FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse
|
||||
ORDER BY CAST(zone AS UNSIGNED), zone"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse]);
|
||||
$output = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$output = $whMgmt->getZonesAll($warehouse);
|
||||
}
|
||||
|
||||
$answer['output'] = $output;
|
||||
|
||||
@@ -1,53 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/StockManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
$warehouse = (int)$data["warehouse"];
|
||||
|
||||
// get warehouse name
|
||||
$sql = "SELECT warehouse_name
|
||||
FROM md_warehouse
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :warehouse
|
||||
";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":warehouse" => $warehouse
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$warehouse = $sth->fetchColumn();
|
||||
|
||||
if(!$warehouse) {
|
||||
$answer["message"] = "Warehouse not found";
|
||||
$stock = new StockManager($pdo2, $company_id);
|
||||
$answer['output'] = $stock->getStockList((int)$data['warehouse'], 'in');
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$table = "td_stock_" . $warehouse;
|
||||
|
||||
$sql = "SELECT a.* , ROUND(a.in,2) as quantity ,
|
||||
b.product_name
|
||||
FROM `{$table}` 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} and
|
||||
a.type = 'in'
|
||||
ORDER BY a.date DESC";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,53 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/StockManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
$warehouse = (int)$data["warehouse"];
|
||||
|
||||
// get warehouse name
|
||||
$sql = "SELECT warehouse_name
|
||||
FROM md_warehouse
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :warehouse
|
||||
";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":warehouse" => $warehouse
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$warehouse = $sth->fetchColumn();
|
||||
|
||||
if(!$warehouse) {
|
||||
$answer["message"] = "Warehouse not found";
|
||||
$stock = new StockManager($pdo2, $company_id);
|
||||
$answer['output'] = $stock->getStockList((int)$data['warehouse'], 'out');
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$table = "td_stock_" . $warehouse;
|
||||
|
||||
$sql = "SELECT a.* , ROUND(a.out,2) as quantity ,
|
||||
b.product_name
|
||||
FROM `{$table}` 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} and
|
||||
a.type = 'out'
|
||||
ORDER BY a.date DESC";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,54 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/StockManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
$warehouse = (int)$data["warehouse"];
|
||||
|
||||
// get warehouse name
|
||||
$sql = "SELECT warehouse_name
|
||||
FROM md_warehouse
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :warehouse
|
||||
";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":warehouse" => $warehouse
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$warehouse = $sth->fetchColumn();
|
||||
|
||||
if(!$warehouse) {
|
||||
$answer["message"] = "Warehouse not found";
|
||||
$stock = new StockManager($pdo2, $company_id);
|
||||
$answer['output'] = $stock->getStockList((int)$data['warehouse'], 'transfer');
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$table = "td_stock_" . $warehouse;
|
||||
|
||||
$sql = "SELECT a.* , ROUND(a.in,2) as quantity,
|
||||
b.product_name
|
||||
FROM `{$table}` 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} and
|
||||
a.type = 'transfer' and
|
||||
a.out > 0
|
||||
ORDER BY a.date DESC";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,66 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$id = 0;
|
||||
$warehouse_id = (int)$data['warehouse'];
|
||||
|
||||
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||
$warehouse = $whMgmt->getStockContext($warehouse_id, $id);
|
||||
$table = $warehouse['table'];
|
||||
|
||||
|
||||
// ── Last 10 stock in/out transactions ────────────────────────────────
|
||||
$sql = "SELECT
|
||||
a.id,
|
||||
a.date,
|
||||
a.product_sku,
|
||||
a.`in` AS stock_in,
|
||||
a.`out` AS stock_out,
|
||||
a.type,
|
||||
a.description,
|
||||
b.product_name
|
||||
FROM `{$table}` 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} and
|
||||
(a.`in` > 0 OR a.`out` > 0)
|
||||
ORDER BY a.date DESC, a.id DESC
|
||||
LIMIT 10";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute();
|
||||
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// ── Build output ─────────────────────────────────────────────────────
|
||||
$activities = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$is_in = (float)$row['stock_in'] > 0;
|
||||
$qty = $is_in ? (float)$row['stock_in'] : (float)$row['stock_out'];
|
||||
|
||||
$activities[] = [
|
||||
'product_name' => $row['product_name'] ?: $row['product_sku'],
|
||||
'sku' => $row['product_sku'],
|
||||
'direction' => $is_in ? 'in' : 'out',
|
||||
'qty' => round($qty, 2),
|
||||
'type' => $row['type'],
|
||||
'description' => $row['description'],
|
||||
'date' => $row['date'],
|
||||
];
|
||||
}
|
||||
|
||||
// ── Response ─────────────────────────────────────────────────────────
|
||||
$output = [
|
||||
'activities' => $activities,
|
||||
];
|
||||
|
||||
$answer["output"] = $output;
|
||||
$answer["success"] = 1;
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$answer['output'] = $report->getWarehouseActivity((int)$data['warehouse']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,70 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$id = 0;
|
||||
$warehouse_id = (int)$data['warehouse'];
|
||||
|
||||
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||
$warehouse = $whMgmt->getStockContext($warehouse_id, $id);
|
||||
$table = $warehouse['table'];
|
||||
|
||||
|
||||
// ── Always last 12 months ────────────────────────────────────────────
|
||||
$now = new DateTime();
|
||||
$start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00');
|
||||
$end = $now->format('Y-m-d 23:59:59');
|
||||
|
||||
// ── Query: group by month ────────────────────────────────────────────
|
||||
$sql = "SELECT
|
||||
DATE_FORMAT(a.date, '%b') AS `label`,
|
||||
DATE_FORMAT(a.date, '%Y-%m') AS `sort_key`,
|
||||
ROUND(SUM(a.`in`), 2) AS `stock_in`,
|
||||
ROUND(SUM(a.`out`), 2) AS `stock_out`
|
||||
FROM `{$table}` a
|
||||
WHERE
|
||||
a.company_id = {$company_id} and
|
||||
a.date >= '{$start}' and
|
||||
a.date <= '{$end}'
|
||||
GROUP BY `sort_key`, `label`
|
||||
ORDER BY `sort_key` ASC";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute();
|
||||
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// ── Index DB results by sort_key (YYYY-MM) ──────────────────────────
|
||||
$dataByMonth = [];
|
||||
foreach ($rows as $row) {
|
||||
$dataByMonth[$row['sort_key']] = $row;
|
||||
}
|
||||
|
||||
// ── Always build 12 months, fill zeros where no data ────────────────
|
||||
$labels = [];
|
||||
$stock_in = [];
|
||||
$stock_out = [];
|
||||
|
||||
for ($i = 11; $i >= 0; $i--) {
|
||||
$dt = (clone $now)->modify("-{$i} months");
|
||||
$key = $dt->format('Y-m'); // e.g. "2025-04"
|
||||
$label = $dt->format('M'); // e.g. "Apr"
|
||||
|
||||
$labels[] = $label;
|
||||
$stock_in[] = isset($dataByMonth[$key]) ? round((float)$dataByMonth[$key]['stock_in'], 2) : 0;
|
||||
$stock_out[] = isset($dataByMonth[$key]) ? round((float)$dataByMonth[$key]['stock_out'], 2) : 0;
|
||||
}
|
||||
|
||||
// ── Response ─────────────────────────────────────────────────────────
|
||||
$output = [
|
||||
'labels' => $labels,
|
||||
'stock_in' => $stock_in,
|
||||
'stock_out' => $stock_out,
|
||||
];
|
||||
|
||||
$answer["output"] = $output;
|
||||
$answer["success"] = 1;
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$answer['output'] = $report->getWarehouseStockMovement((int)$data['warehouse']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,84 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$id = 0;
|
||||
$warehouse_id = (int)$data['warehouse'];
|
||||
|
||||
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||
$warehouse = $whMgmt->getStockContext($warehouse_id, $id);
|
||||
$table = $warehouse['table'];
|
||||
|
||||
|
||||
// ── Always last 12 months ────────────────────────────────────────────
|
||||
$now = new DateTime();
|
||||
$start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00');
|
||||
$end = $now->format('Y-m-d 23:59:59');
|
||||
|
||||
// ── Get cumulative balance BEFORE the 12-month window ────────────────
|
||||
$sql_prior = "SELECT
|
||||
ROUND(COALESCE(SUM(a.`in`), 0) - COALESCE(SUM(a.`out`), 0), 2) AS `balance`
|
||||
FROM `{$table}` a
|
||||
WHERE
|
||||
a.company_id = {$company_id} and
|
||||
a.date < '{$start}'";
|
||||
|
||||
$sth = $pdo2->prepare($sql_prior);
|
||||
$sth->execute();
|
||||
$prior_balance = (float)$sth->fetchColumn();
|
||||
|
||||
// ── Get monthly in/out for the 12-month window ──────────────────────
|
||||
$sql = "SELECT
|
||||
DATE_FORMAT(a.date, '%Y-%m') AS `sort_key`,
|
||||
ROUND(SUM(a.`in`), 2) AS `stock_in`,
|
||||
ROUND(SUM(a.`out`), 2) AS `stock_out`
|
||||
FROM `{$table}` a
|
||||
WHERE
|
||||
a.company_id = {$company_id} and
|
||||
a.date >= '{$start}' and
|
||||
a.date <= '{$end}'
|
||||
GROUP BY `sort_key`
|
||||
ORDER BY `sort_key` ASC";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute();
|
||||
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// ── Index DB results by sort_key (YYYY-MM) ──────────────────────────
|
||||
$dataByMonth = [];
|
||||
foreach ($rows as $row) {
|
||||
$dataByMonth[$row['sort_key']] = $row;
|
||||
}
|
||||
|
||||
// ── Always build 12 months, accumulate running balance ──────────────
|
||||
$labels = [];
|
||||
$balance = [];
|
||||
$running = $prior_balance;
|
||||
|
||||
for ($i = 11; $i >= 0; $i--) {
|
||||
$dt = (clone $now)->modify("-{$i} months");
|
||||
$key = $dt->format('Y-m');
|
||||
$label = $dt->format('M');
|
||||
|
||||
$month_in = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_in'] : 0;
|
||||
$month_out = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_out'] : 0;
|
||||
|
||||
$running += ($month_in - $month_out);
|
||||
|
||||
$labels[] = $label;
|
||||
$balance[] = round($running, 2);
|
||||
}
|
||||
|
||||
// ── Response ─────────────────────────────────────────────────────────
|
||||
$output = [
|
||||
'labels' => $labels,
|
||||
'balance' => $balance,
|
||||
];
|
||||
|
||||
$answer["output"] = $output;
|
||||
$answer["success"] = 1;
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$answer['output'] = $report->getWarehouseStockTrend((int)$data['warehouse']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,24 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
|
||||
$sql = "SELECT SUM(total_in) as total_in, SUM(total_out) as total_out, COUNT(DISTINCT warehouse_id) as total_warehouse
|
||||
FROM warehouse_balance 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["success"] = 1;
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$answer['output'] = $report->getWarehouseBalanceSummary();
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,74 +1,24 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) {
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
$sql = "SELECT `log` FROM md_product_category
|
||||
WHERE company_id = :company_id AND id = :id";
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true);
|
||||
$table_log[] = $logging;
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$sql = "UPDATE md_product_category SET
|
||||
`category` = :category,
|
||||
`slug` = :slug,
|
||||
`description` = :description,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":id" => $id,
|
||||
":company_id" => $company_id,
|
||||
":category" => $data["category"],
|
||||
":slug" => $data["slug"],
|
||||
":description" => $data["description"],
|
||||
":status" => $data["status"],
|
||||
":log" => json_encode($table_log),
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
||||
$sql = "INSERT INTO md_product_category
|
||||
(company_id, category, slug, `description`, `status`, `log`)
|
||||
VALUES
|
||||
(:company_id, :category, :slug, :description, :status, :log)";
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":category" => $data["category"],
|
||||
":slug" => $data["slug"],
|
||||
":description" => $data["description"],
|
||||
":status" => $data["status"],
|
||||
":log" => json_encode($table_log),
|
||||
]);
|
||||
}
|
||||
|
||||
$product = new ProductManager($pdo, $company_id);
|
||||
$product->saveCategory($data, $logging);
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Database error, please try again';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,144 +1,69 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/FileUploader.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
// ── Business rule: min_stock < reorder_point ──────────────────────────
|
||||
// The warning tier (balance between min_stock and reorder_point) only
|
||||
// exists when min_stock is strictly less than reorder_point. Enforcing
|
||||
// this invariant keeps the low-stock report meaningful.
|
||||
$min_stock = (float)($data['min_stock'] ?? 0);
|
||||
$reorder_point = (float)($data['reorder_point'] ?? 0);
|
||||
|
||||
if ($min_stock < 0 || $reorder_point < 0) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Min stock and reorder point cannot be negative.";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Min stock and reorder point cannot be negative.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
if ($min_stock >= $reorder_point) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Min stock ({$min_stock}) must be less than reorder point ({$reorder_point}).";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = "Min stock ({$min_stock}) must be less than reorder point ({$reorder_point}).";
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
/** 1. FILE HANDLING **/
|
||||
$uploader = new FileUploader($include_url . "uploads/product/");
|
||||
$uploader = new FileUploader($include_url . 'uploads/product/');
|
||||
|
||||
if (!empty($_FILES['product_image']['name'])) {
|
||||
// Cleanup old image when updating
|
||||
if ($id > 0) {
|
||||
$sql = "SELECT `product_image` FROM md_product WHERE company_id = :company_id AND id = :id";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([":company_id" => $company_id, ":id" => $id]);
|
||||
$existing_files = $sth->fetchColumn();
|
||||
|
||||
$uploader->cleanup($existing_files, '');
|
||||
$sth->execute([':company_id' => $company_id, ':id' => $id]);
|
||||
$uploader->cleanup($sth->fetchColumn(), '');
|
||||
}
|
||||
|
||||
$errors = $uploader->upload('product_image');
|
||||
if (!empty($errors)) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $errors[0];
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $errors[0];
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
// Get the single uploaded filename directly
|
||||
$db_image_string = $uploader->getUploadedFiles()[0] ?? '';
|
||||
|
||||
} else {
|
||||
// No new file — keep existing, skip cleanup entirely
|
||||
$db_image_string = $data['keep_files'] ?? '';
|
||||
}
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $id, $db_image_string) {
|
||||
|
||||
// get existing log
|
||||
$sql = "SELECT `log` FROM md_product
|
||||
WHERE company_id = :company_id AND id = :id";
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true);
|
||||
$table_log[] = $logging;
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$sql = "UPDATE md_product SET
|
||||
product_name = :product_name,
|
||||
sku = :sku,
|
||||
price = :price,
|
||||
min_stock = :min_stock,
|
||||
reorder_point = :reorder_point,
|
||||
category = :category,
|
||||
product_image = :product_image,
|
||||
`description` = :description,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":id" => $id,
|
||||
":company_id" => $company_id,
|
||||
":product_name" => $data["product_name"],
|
||||
":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"],
|
||||
":status" => $data["status"],
|
||||
":log" => json_encode($table_log),
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
||||
$sql = "INSERT INTO md_product
|
||||
(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, :reorder_point, :category, :product_image, :description, :status, :log)";
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":product_name" => $data["product_name"],
|
||||
":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"],
|
||||
":status" => $data["status"],
|
||||
":log" => json_encode($table_log),
|
||||
]);
|
||||
}
|
||||
|
||||
/** 2. DATABASE **/
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $db_image_string) {
|
||||
$product = new ProductManager($pdo, $company_id);
|
||||
$product->saveProduct($data, $logging, $db_image_string);
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
if (isset($uploader)) $uploader->rollbackUploads();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Database error, please try again';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
$answer["error"] = $e->getMessage();
|
||||
if (isset($uploader)) $uploader->rollbackUploads();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,92 +5,20 @@
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) {
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
$sql = "SELECT `log` FROM md_storage
|
||||
WHERE company_id = :company_id AND id = :id";
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true);
|
||||
if (!is_array($table_log)) $table_log = [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
":company_id" => $company_id,
|
||||
":warehouse" => $data["warehouse"] ?? "",
|
||||
":zone" => $data["zone"] ?? "",
|
||||
":aisle_from" => $data["aisle_from"] ?? "",
|
||||
":aisle_to" => $data["aisle_to"] ?? "",
|
||||
":rack_from" => $data["rack_from"] ?? "",
|
||||
":rack_to" => $data["rack_to"] ?? "",
|
||||
":description" => $data["description"] ?? "",
|
||||
":status" => (int)($data["status"] ?? 1),
|
||||
":log" => json_encode($table_log, JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$sql = "UPDATE md_storage SET
|
||||
warehouse = :warehouse,
|
||||
zone = :zone,
|
||||
aisle_from = :aisle_from,
|
||||
aisle_to = :aisle_to,
|
||||
rack_from = :rack_from,
|
||||
rack_to = :rack_to,
|
||||
description = :description,
|
||||
status = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
|
||||
$params[":id"] = $id;
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute($params);
|
||||
|
||||
$storage_id = $id;
|
||||
|
||||
} else {
|
||||
|
||||
$sql = "INSERT INTO md_storage
|
||||
(company_id, warehouse, zone, aisle_from, aisle_to,
|
||||
rack_from, rack_to, description, status, `log`)
|
||||
VALUES
|
||||
(:company_id, :warehouse, :zone, :aisle_from, :aisle_to,
|
||||
:rack_from, :rack_to, :description, :status, :log)";
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute($params);
|
||||
|
||||
$storage_id = (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// Sync md_rack rows — same $pdo, inside the same transaction
|
||||
$wm = new WarehouseManager($pdo, $company_id);
|
||||
$wm->syncRacks($storage_id, [
|
||||
'warehouse' => $data['warehouse'],
|
||||
'zone' => $data['zone'],
|
||||
'aisle_from' => $data['aisle_from'],
|
||||
'aisle_to' => $data['aisle_to'],
|
||||
'rack_from' => $data['rack_from'],
|
||||
'rack_to' => $data['rack_to'],
|
||||
]);
|
||||
$wh = new WarehouseManager($pdo, $company_id);
|
||||
$wh->saveStorage($data, $logging);
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Database error, please try again';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,77 +1,24 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) {
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
$sql = "SELECT `log` FROM md_warehouse
|
||||
WHERE company_id = :company_id AND id = :id";
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
|
||||
$table_log = json_decode($sth->fetchColumn() ?: '[]', true);
|
||||
if (!is_array($table_log)) $table_log = [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
$params = [
|
||||
":company_id" => $company_id,
|
||||
":warehouse_name" => $data["warehouse_name"] ?? "",
|
||||
":location" => $data["location"] ?? "",
|
||||
":manager" => (int)($data["manager"] ?? 0),
|
||||
":description" => $data["description"] ?? "",
|
||||
":status" => (int)($data["status"] ?? 1),
|
||||
":log" => json_encode($table_log, JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$sql = "UPDATE md_warehouse SET
|
||||
`warehouse_name` = :warehouse_name,
|
||||
`location` = :location,
|
||||
`manager` = :manager,
|
||||
`description` = :description,
|
||||
`status` = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id";
|
||||
|
||||
$params[":id"] = $id;
|
||||
|
||||
} else {
|
||||
|
||||
$sql = "INSERT INTO md_warehouse
|
||||
(company_id, warehouse_name, location, manager, description, status, `log`)
|
||||
VALUES
|
||||
(:company_id, :warehouse_name, :location, :manager, :description, :status, :log)";
|
||||
}
|
||||
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute($params);
|
||||
|
||||
// Create stock table for new warehouse
|
||||
if ($id == 0 && !empty($data["warehouse_name"])) {
|
||||
$safe_name = 'td_stock_' . preg_replace('/[^a-zA-Z0-9_]/', '', $data["warehouse_name"]);
|
||||
$sql = "CREATE TABLE `$safe_name` LIKE `td_stock`";
|
||||
$pdo->exec($sql);
|
||||
}
|
||||
|
||||
$wh = new WarehouseManager($pdo, $company_id);
|
||||
$wh->saveWarehouse($data, $logging);
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = 'Database error, please try again';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
|
||||
$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
|
||||
a.company_id = {$company_id}
|
||||
GROUP BY a.id";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$product = new ProductManager($pdo2, $company_id);
|
||||
$answer['output'] = $product->getProductList();
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,24 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
|
||||
$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
|
||||
a.company_id = {$company_id}
|
||||
GROUP BY a.id";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$product = new ProductManager($pdo2, $company_id);
|
||||
$answer['output'] = $product->getCategoryList();
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,25 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_product_category
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$product = new ProductManager($pdo2, $company_id);
|
||||
$answer['output'] = $product->getCategoryById((int)$data['id']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,25 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_product
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$product = new ProductManager($pdo2, $company_id);
|
||||
$answer['output'] = $product->getProductById((int)$data['id']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,25 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_storage
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$wh = new WarehouseManager($pdo2, $company_id);
|
||||
$answer['output'] = $wh->getStorageById((int)$data['id']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,25 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
$id = (int)$data['id'];
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM md_warehouse
|
||||
WHERE
|
||||
company_id = :company_id and
|
||||
id = :id ";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$wh = new WarehouseManager($pdo2, $company_id);
|
||||
$answer['output'] = $wh->getWarehouseById((int)$data['id']);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,29 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
$sql = "SELECT a.*, b.warehouse_name
|
||||
FROM md_storage a
|
||||
LEFT JOIN md_warehouse b ON
|
||||
a.company_id = b.company_id and
|
||||
a.warehouse = b.id
|
||||
WHERE
|
||||
a.company_id = :company_id ";
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// modify output
|
||||
foreach($answer["output"] as $key => $item){
|
||||
|
||||
}
|
||||
|
||||
$answer["success"] = 1;
|
||||
$wh = new WarehouseManager($pdo2, $company_id);
|
||||
$answer['output'] = $wh->getStorageList();
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
@@ -1,36 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
$sql = "SELECT
|
||||
a.*,
|
||||
COALESCE(r.total_racks, 0) AS capacity,
|
||||
COALESCE(r.occupied_racks, 0) AS space_used,
|
||||
COALESCE(r.unique_product, 0) AS unique_product,
|
||||
c.name AS manager_name,
|
||||
c.surname AS manager_surname
|
||||
FROM md_warehouse a
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
company_id, warehouse,
|
||||
COUNT(*) AS total_racks,
|
||||
COUNT(product_sku) AS occupied_racks,
|
||||
COUNT(DISTINCT product_sku) AS unique_product
|
||||
FROM md_rack
|
||||
GROUP BY company_id, warehouse
|
||||
) r ON a.company_id = r.company_id AND a.id = r.warehouse
|
||||
LEFT JOIN wms.user c ON a.manager = c.user_id
|
||||
WHERE a.company_id = :company_id";
|
||||
|
||||
$sth = $pdo2->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id
|
||||
]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer["success"] = 1;
|
||||
$wh = new WarehouseManager($pdo2, $company_id);
|
||||
$answer['output'] = $wh->getWarehouseListAll();
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user