remove actions and data integrity
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ContactManager
|
||||
*
|
||||
* Encapsulates contact and contact type operations:
|
||||
* - Soft-delete with downstream validation
|
||||
*
|
||||
* Note: Methods that modify data do NOT manage their own DB transactions.
|
||||
* Callers are responsible for wrapping operations in dbTransaction() when atomicity is needed.
|
||||
*/
|
||||
class ContactManager {
|
||||
|
||||
private $pdo;
|
||||
private $company_id;
|
||||
|
||||
public function __construct($pdo, $company_id) {
|
||||
$this->pdo = $pdo;
|
||||
$this->company_id = $company_id;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Private helpers
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Check if a contact_id is referenced in any active td_stock_* row.
|
||||
* Returns true if blocking stock exists.
|
||||
*/
|
||||
private function hasActiveStock(int $contact_id): bool {
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name LIKE 'td_stock_%'"
|
||||
);
|
||||
$sth->execute();
|
||||
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM `$table`
|
||||
WHERE company_id = :company_id
|
||||
AND contact_id = :contact_id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':contact_id' => $contact_id,
|
||||
]);
|
||||
if ($sth->fetchColumn() > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a log entry array for audit context.
|
||||
*/
|
||||
private function buildLogEntry(string $action): array {
|
||||
return [
|
||||
'user_id' => $_SESSION['login_user_id'] ?? null,
|
||||
'dt' => date('Y-m-d H:i:s'),
|
||||
'login' => isset($_SESSION['otpTime'])
|
||||
? date('Y-m-d H:i:s', $_SESSION['otpTime'])
|
||||
: null,
|
||||
'action' => $action,
|
||||
];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Contact type
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Soft-delete a contact type.
|
||||
*
|
||||
* Blocks if any md_contact references this type.
|
||||
*
|
||||
* @throws Exception if contact type not found or has dependent contacts
|
||||
*/
|
||||
public function deleteContactType(int $type_id): void {
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, contact_type, `log` FROM md_contact_type
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':id' => $type_id,
|
||||
]);
|
||||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
throw new Exception("Contact type not found.");
|
||||
}
|
||||
|
||||
// Block if any contact references this type
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM md_contact
|
||||
WHERE company_id = :company_id
|
||||
AND contact_type = :type_id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':type_id' => $type_id,
|
||||
]);
|
||||
|
||||
if ($sth->fetchColumn() > 0) {
|
||||
throw new Exception(
|
||||
"Cannot delete — contact type \"{$row['contact_type']}\" " .
|
||||
"still has contacts assigned to it."
|
||||
);
|
||||
}
|
||||
|
||||
// Append delete event to log
|
||||
$log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$log[] = $this->buildLogEntry('delete');
|
||||
|
||||
// Soft-delete
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_contact_type
|
||||
SET company_id = company_id * -1,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':log' => json_encode($log),
|
||||
':id' => $type_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Contact
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Soft-delete a contact.
|
||||
*
|
||||
* Blocks if the contact is referenced in any active td_stock_* row.
|
||||
*
|
||||
* @throws Exception if contact not found or referenced in active stock
|
||||
*/
|
||||
public function deleteContact(int $contact_id): void {
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, contact_name, `log` FROM md_contact
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':id' => $contact_id,
|
||||
]);
|
||||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
throw new Exception("Contact not found.");
|
||||
}
|
||||
|
||||
// Block if contact is referenced in any active stock transaction
|
||||
if ($this->hasActiveStock($contact_id)) {
|
||||
throw new Exception(
|
||||
"Cannot delete — \"{$row['contact_name']}\" " .
|
||||
"is referenced in active stock transactions."
|
||||
);
|
||||
}
|
||||
|
||||
// Append delete event to log
|
||||
$log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$log[] = $this->buildLogEntry('delete');
|
||||
|
||||
// Soft-delete
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_contact
|
||||
SET company_id = company_id * -1,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':log' => json_encode($log),
|
||||
':id' => $contact_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ProductManager
|
||||
*
|
||||
* Encapsulates product and product category operations:
|
||||
* - Soft-delete with downstream validation
|
||||
*
|
||||
* Note: Methods that modify data do NOT manage their own DB transactions.
|
||||
* Callers are responsible for wrapping operations in dbTransaction() when atomicity is needed.
|
||||
*/
|
||||
class ProductManager {
|
||||
|
||||
private $pdo;
|
||||
private $company_id;
|
||||
|
||||
public function __construct($pdo, $company_id) {
|
||||
$this->pdo = $pdo;
|
||||
$this->company_id = $company_id;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Private helpers
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Check if a SKU has any active stock across all td_stock_* tables.
|
||||
* Returns the first blocking table name found, or null if clear.
|
||||
*/
|
||||
private function findActiveStock(string $sku): ?string {
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name LIKE 'td_stock_%'"
|
||||
);
|
||||
$sth->execute();
|
||||
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM `$table`
|
||||
WHERE company_id = :company_id
|
||||
AND product_sku = :sku"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':sku' => $sku,
|
||||
]);
|
||||
if ($sth->fetchColumn() > 0) {
|
||||
return $table;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a log entry array for audit context.
|
||||
*/
|
||||
private function buildLogEntry(string $action): array {
|
||||
return [
|
||||
'user_id' => $_SESSION['login_user_id'] ?? null,
|
||||
'dt' => date('Y-m-d H:i:s'),
|
||||
'login' => isset($_SESSION['otpTime'])
|
||||
? date('Y-m-d H:i:s', $_SESSION['otpTime'])
|
||||
: null,
|
||||
'action' => $action,
|
||||
];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Product category
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Soft-delete a product category.
|
||||
*
|
||||
* Blocks if any md_product references this category.
|
||||
*
|
||||
* @throws Exception if category not found or has dependent products
|
||||
*/
|
||||
public function deleteCategory(int $category_id): void {
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, category, `log` FROM md_product_category
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':id' => $category_id,
|
||||
]);
|
||||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
throw new Exception("Product category not found.");
|
||||
}
|
||||
|
||||
// Block if any product references this category
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM md_product
|
||||
WHERE company_id = :company_id
|
||||
AND category = :category_id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':category_id' => $category_id,
|
||||
]);
|
||||
|
||||
if ($sth->fetchColumn() > 0) {
|
||||
throw new Exception(
|
||||
"Cannot delete — category \"{$row['category']}\" " .
|
||||
"still has products assigned to it."
|
||||
);
|
||||
}
|
||||
|
||||
// Append delete event to log
|
||||
$log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$log[] = $this->buildLogEntry('delete');
|
||||
|
||||
// Soft-delete
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_product_category
|
||||
SET company_id = company_id * -1,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':log' => json_encode($log),
|
||||
':id' => $category_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Product
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Soft-delete a product.
|
||||
*
|
||||
* Blocks if the product SKU has any active stock across any td_stock_* table.
|
||||
*
|
||||
* @throws Exception if product not found or has active stock
|
||||
*/
|
||||
public function deleteProduct(int $product_id): void {
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, product_name, sku, `log` FROM md_product
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':id' => $product_id,
|
||||
]);
|
||||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
throw new Exception("Product not found.");
|
||||
}
|
||||
|
||||
// Block if active stock exists for this SKU in any warehouse
|
||||
$blocking_table = $this->findActiveStock($row['sku']);
|
||||
if ($blocking_table !== null) {
|
||||
throw new Exception(
|
||||
"Cannot delete — \"{$row['product_name']}\" " .
|
||||
"still has active stock in the system."
|
||||
);
|
||||
}
|
||||
|
||||
// Append delete event to log
|
||||
$log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$log[] = $this->buildLogEntry('delete');
|
||||
|
||||
// Soft-delete
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_product
|
||||
SET company_id = company_id * -1,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':log' => json_encode($log),
|
||||
':id' => $product_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -125,8 +125,22 @@ class WarehouseManager {
|
||||
*/
|
||||
public function syncRacks(int $storage_id, array $range): void {
|
||||
|
||||
// Guard: refuse if any rack under this storage_id is still occupied
|
||||
$this->validateRangeChange($storage_id, $range);
|
||||
$this->removeRacksOutsideRange($storage_id, $range);
|
||||
|
||||
// Wipe all empty racks belonging to this storage_id
|
||||
$sql = "DELETE FROM md_rack
|
||||
WHERE company_id = :company_id
|
||||
AND storage_id = :storage_id
|
||||
AND product_sku IS NULL";
|
||||
|
||||
$sth = $this->pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':storage_id' => $storage_id,
|
||||
]);
|
||||
|
||||
// Reinsert the full range fresh
|
||||
$this->insertRacksInRange($storage_id, $range);
|
||||
}
|
||||
|
||||
@@ -161,32 +175,6 @@ class WarehouseManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete empty racks that fall outside the new range.
|
||||
*/
|
||||
private function removeRacksOutsideRange(int $storage_id, array $range): void {
|
||||
|
||||
$sql = "DELETE FROM md_rack
|
||||
WHERE company_id = :company_id
|
||||
AND storage_id = :storage_id
|
||||
AND (
|
||||
CAST(aisle AS UNSIGNED) < :aisle_from OR
|
||||
CAST(aisle AS UNSIGNED) > :aisle_to OR
|
||||
CAST(rack AS UNSIGNED) < :rack_from OR
|
||||
CAST(rack AS UNSIGNED) > :rack_to
|
||||
)";
|
||||
|
||||
$sth = $this->pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $this->company_id,
|
||||
":storage_id" => $storage_id,
|
||||
":aisle_from" => (int) $range['aisle_from'],
|
||||
":aisle_to" => (int) $range['aisle_to'],
|
||||
":rack_from" => (int) $range['rack_from'],
|
||||
":rack_to" => (int) $range['rack_to'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert rack rows for the full range (IGNORE skips duplicates).
|
||||
*/
|
||||
@@ -266,24 +254,30 @@ class WarehouseManager {
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Build a log JSON by appending an action entry to an existing md_rack.log.
|
||||
* Entry captures user_id, dt (now), and login (session start) for audit context,
|
||||
* Insert a log entry into md_rack_log for the given rack and action.
|
||||
* Captures user_id, dt (now), and login (session start) for audit context,
|
||||
* plus any action-specific fields passed via $extra.
|
||||
*/
|
||||
private function buildRackLog(?string $existing_log, string $action, array $extra = []): string {
|
||||
private function insertRackLog(int $rack_id, string $action, array $extra = []): void {
|
||||
|
||||
$log = json_decode($existing_log ?: '[]', true) ?: [];
|
||||
$sql = "INSERT INTO md_rack_log
|
||||
(company_id, md_rack_id, user_id, dt, login, action, product_sku, td_stock_id)
|
||||
VALUES
|
||||
(:company_id, :md_rack_id, :user_id, :dt, :login, :action, :product_sku, :td_stock_id)";
|
||||
|
||||
$log[] = array_merge([
|
||||
'user_id' => $_SESSION['login_user_id'] ?? null,
|
||||
'dt' => date('Y-m-d H:i:s'),
|
||||
'login' => isset($_SESSION['otpTime'])
|
||||
$sth = $this->pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':md_rack_id' => $rack_id,
|
||||
':user_id' => $_SESSION['login_user_id'] ?? null,
|
||||
':dt' => date('Y-m-d H:i:s'),
|
||||
':login' => isset($_SESSION['otpTime'])
|
||||
? date('Y-m-d H:i:s', $_SESSION['otpTime'])
|
||||
: null,
|
||||
'action' => $action,
|
||||
], $extra);
|
||||
|
||||
return json_encode($log, JSON_UNESCAPED_UNICODE);
|
||||
':action' => $action,
|
||||
':product_sku' => $extra['product_sku'] ?? null,
|
||||
':td_stock_id' => $extra['td_stock_id'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,8 +290,8 @@ class WarehouseManager {
|
||||
*/
|
||||
public function occupyRack($warehouse_id, $zone, $aisle, $rack, $product_sku, $td_stock_id): void {
|
||||
|
||||
// Lock and fetch current state (including log for in-place append)
|
||||
$sql = "SELECT id, product_sku, `log` FROM md_rack
|
||||
|
||||
$sql = "SELECT id, product_sku FROM md_rack
|
||||
WHERE company_id = :company_id
|
||||
AND warehouse = :warehouse
|
||||
AND zone = :zone
|
||||
@@ -328,19 +322,19 @@ class WarehouseManager {
|
||||
// Single atomic UPDATE: state + log together
|
||||
$sql = "UPDATE md_rack
|
||||
SET product_sku = :product_sku,
|
||||
td_stock_id = :td_stock_id,
|
||||
`log` = :log
|
||||
td_stock_id = :td_stock_id
|
||||
WHERE id = :id";
|
||||
|
||||
$sth = $this->pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":product_sku" => $product_sku,
|
||||
":td_stock_id" => $td_stock_id,
|
||||
":log" => $this->buildRackLog($current['log'], 'occupy', [
|
||||
":id" => $current['id'],
|
||||
]);
|
||||
|
||||
$this->insertRackLog($current['id'], 'occupy', [
|
||||
'product_sku' => $product_sku,
|
||||
'td_stock_id' => $td_stock_id,
|
||||
]),
|
||||
":id" => $current['id'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -349,8 +343,8 @@ class WarehouseManager {
|
||||
*/
|
||||
public function releaseRack($warehouse_id, $zone, $aisle, $rack): void {
|
||||
|
||||
// Fetch current state + log (needed to append release event)
|
||||
$sql = "SELECT id, product_sku, td_stock_id, `log` FROM md_rack
|
||||
|
||||
$sql = "SELECT id, product_sku, td_stock_id FROM md_rack
|
||||
WHERE company_id = :company_id
|
||||
AND warehouse = :warehouse
|
||||
AND zone = :zone
|
||||
@@ -376,17 +370,17 @@ class WarehouseManager {
|
||||
// Clear state + record log in one UPDATE
|
||||
$sql = "UPDATE md_rack
|
||||
SET product_sku = NULL,
|
||||
td_stock_id = NULL,
|
||||
`log` = :log
|
||||
td_stock_id = NULL
|
||||
WHERE id = :id";
|
||||
|
||||
$sth = $this->pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":log" => $this->buildRackLog($current['log'], 'release', [
|
||||
":id" => $current['id'],
|
||||
]);
|
||||
|
||||
$this->insertRackLog($current['id'], 'release', [
|
||||
'product_sku' => $current['product_sku'],
|
||||
'td_stock_id' => $current['td_stock_id'],
|
||||
]),
|
||||
":id" => $current['id'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -403,7 +397,7 @@ class WarehouseManager {
|
||||
): void {
|
||||
|
||||
// Fetch both racks with FOR UPDATE, ordered by id to prevent deadlocks
|
||||
$sql = "SELECT id, warehouse, zone, aisle, rack, product_sku, td_stock_id, `log`
|
||||
$sql = "SELECT id, warehouse, zone, aisle, rack, product_sku, td_stock_id
|
||||
FROM md_rack
|
||||
WHERE company_id = :company_id
|
||||
AND (
|
||||
@@ -464,32 +458,443 @@ class WarehouseManager {
|
||||
|
||||
// Clear source — state + log in one UPDATE
|
||||
$sql = "UPDATE md_rack
|
||||
SET product_sku = NULL, td_stock_id = NULL, `log` = :log
|
||||
SET product_sku = NULL, td_stock_id = NULL
|
||||
WHERE id = :id";
|
||||
$this->pdo->prepare($sql)->execute([
|
||||
":log" => $this->buildRackLog($from['log'], 'transfer_out', [
|
||||
":id" => $from['id'],
|
||||
]);
|
||||
|
||||
$this->insertRackLog($from['id'], 'transfer_out', [
|
||||
'product_sku' => $sku,
|
||||
'td_stock_id' => $td_stock_id,
|
||||
'to_rack_id' => $to['id'],
|
||||
]),
|
||||
":id" => $from['id'],
|
||||
]);
|
||||
|
||||
// Populate destination — state + log in one UPDATE
|
||||
$sql = "UPDATE md_rack
|
||||
SET product_sku = :sku, td_stock_id = :td_stock_id, `log` = :log
|
||||
SET product_sku = :sku, td_stock_id = :td_stock_id
|
||||
WHERE id = :id";
|
||||
$this->pdo->prepare($sql)->execute([
|
||||
":sku" => $sku,
|
||||
":td_stock_id" => $td_stock_id,
|
||||
":log" => $this->buildRackLog($to['log'], 'transfer_in', [
|
||||
'product_sku' => $sku,
|
||||
'td_stock_id' => $td_stock_id,
|
||||
'from_rack_id' => $from['id'],
|
||||
]),
|
||||
":id" => $to['id'],
|
||||
]);
|
||||
|
||||
$this->insertRackLog($to['id'], 'transfer_in', [
|
||||
'product_sku' => $sku,
|
||||
'td_stock_id' => $td_stock_id,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Delete (soft-delete) operations
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Validate that a given row is the latest transaction for its SKU in the table.
|
||||
* Throws a descriptive exception if a newer transaction exists.
|
||||
*
|
||||
* @param string $product_sku SKU to check
|
||||
* @param string $row_date The date of the row being deleted
|
||||
* @param string $product_name Human-readable product name for error message
|
||||
* @throws Exception if a newer transaction exists
|
||||
*/
|
||||
private function validateLatestTransaction(
|
||||
string $product_sku,
|
||||
string $row_date,
|
||||
string $product_name
|
||||
): void {
|
||||
|
||||
// Discover all td_stock_* tables for this database
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name LIKE 'td_stock_%'"
|
||||
);
|
||||
$sth->execute();
|
||||
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($tables)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build UNION across all td_stock_* tables to find global MAX(date)
|
||||
$unions = implode(' UNION ALL ', array_map(
|
||||
fn($t) => "SELECT `type`, `date` FROM `$t`
|
||||
WHERE company_id = :company_id
|
||||
AND product_sku = :product_sku",
|
||||
$tables
|
||||
));
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT `type`, `date` FROM ($unions) AS all_stock
|
||||
ORDER BY `date` DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':product_sku' => $product_sku,
|
||||
]);
|
||||
$latest = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$latest || $row_date === $latest['date']) {
|
||||
return; // ✓ this is the latest globally — allow
|
||||
}
|
||||
|
||||
$type = strtoupper($latest['type']);
|
||||
$date = $latest['date'];
|
||||
|
||||
throw new Exception(
|
||||
"Cannot delete — \"{$product_name}\" has a newer " .
|
||||
"{$type} transaction on {$date} that must be deleted first."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to build a log entry with consistent user and timestamp info for warehouse operations.
|
||||
*/
|
||||
private function buildLogEntry(string $action): array {
|
||||
return [
|
||||
'user_id' => $_SESSION['login_user_id'] ?? null,
|
||||
'dt' => date('Y-m-d H:i:s'),
|
||||
'login' => isset($_SESSION['otpTime'])
|
||||
? date('Y-m-d H:i:s', $_SESSION['otpTime'])
|
||||
: null,
|
||||
'action' => $action,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve product name from md_product for readable error messages.
|
||||
*/
|
||||
private function getProductName(string $product_sku): string {
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT product_name FROM md_product
|
||||
WHERE company_id = :company_id AND sku = :sku"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':sku' => $product_sku,
|
||||
]);
|
||||
return $sth->fetchColumn() ?: $product_sku;
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete a stock_in record and reverse its side effects.
|
||||
*
|
||||
* - Validates this is the latest transaction for the SKU
|
||||
* - Negates company_id (soft-delete)
|
||||
* - Releases the rack
|
||||
* - Reverses the balance
|
||||
*
|
||||
* Must be called inside a DB transaction by the caller.
|
||||
*
|
||||
* @throws Exception if not the latest transaction or record not found
|
||||
*/
|
||||
public function deleteStockIn(int $stock_id, int $warehouse_id): void {
|
||||
|
||||
$ctx = $this->getStockContext($warehouse_id, $stock_id);
|
||||
$table = $ctx['table'];
|
||||
$row = $ctx['row'];
|
||||
|
||||
if (!$row) {
|
||||
throw new Exception("Stock in record not found.");
|
||||
}
|
||||
|
||||
$product_sku = $row['product_sku'];
|
||||
$product_name = $this->getProductName($product_sku);
|
||||
|
||||
$this->validateLatestTransaction($product_sku, $row['date'], $product_name);
|
||||
|
||||
// Soft-delete
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `$table`
|
||||
SET company_id = company_id * -1
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([':id' => $stock_id, ':company_id' => $this->company_id]);
|
||||
|
||||
// Release the rack
|
||||
$this->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']);
|
||||
|
||||
// Reverse balance
|
||||
$this->adjustBalance('in', $warehouse_id, $product_sku, (int)$row['in'], 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete a stock_out record and reverse its side effects.
|
||||
*
|
||||
* - Validates this is the latest transaction for the SKU
|
||||
* - Negates company_id (soft-delete)
|
||||
* - Re-occupies the rack with the original stock_in batch (via ref_id)
|
||||
* - Reverses the balance
|
||||
*
|
||||
* Must be called inside a DB transaction by the caller.
|
||||
*
|
||||
* @throws Exception if not the latest transaction or record not found
|
||||
*/
|
||||
public function deleteStockOut(int $stock_id, int $warehouse_id): void {
|
||||
|
||||
$ctx = $this->getStockContext($warehouse_id, $stock_id);
|
||||
$table = $ctx['table'];
|
||||
$row = $ctx['row'];
|
||||
|
||||
if (!$row) {
|
||||
throw new Exception("Stock out record not found.");
|
||||
}
|
||||
|
||||
$product_sku = $row['product_sku'];
|
||||
$product_name = $this->getProductName($product_sku);
|
||||
|
||||
$this->validateLatestTransaction($product_sku, $row['date'], $product_name);
|
||||
|
||||
// Soft-delete
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `$table`
|
||||
SET company_id = company_id * -1
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([':id' => $stock_id, ':company_id' => $this->company_id]);
|
||||
|
||||
// Re-occupy the rack — restore the stock_in batch that was consumed
|
||||
$this->occupyRack(
|
||||
$warehouse_id,
|
||||
$row['zone'], $row['aisle'], $row['rack'],
|
||||
$product_sku,
|
||||
(int)$row['ref_id']
|
||||
);
|
||||
|
||||
// Reverse balance
|
||||
$this->adjustBalance('out', $warehouse_id, $product_sku, (int)$row['out'], 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete a transfer record pair and reverse all side effects.
|
||||
*
|
||||
* - Validates this is the latest transaction for the SKU on BOTH warehouses
|
||||
* - Negates company_id on both rows (soft-delete)
|
||||
* - Releases destination rack, re-occupies source rack
|
||||
* - Reverses balances on both warehouses
|
||||
*
|
||||
* Must be called inside a DB transaction by the caller.
|
||||
*
|
||||
* @throws Exception if not the latest transaction on either side, or records not found
|
||||
*/
|
||||
public function deleteTransfer(int $from_stock_id, int $from_warehouse_id): void {
|
||||
|
||||
// Fetch the "from" row (transfer_out side)
|
||||
$from_ctx = $this->getStockContext($from_warehouse_id, $from_stock_id);
|
||||
$from_table = $from_ctx['table'];
|
||||
$from_row = $from_ctx['row'];
|
||||
|
||||
if (!$from_row || $from_row['type'] !== 'transfer') {
|
||||
throw new Exception("Transfer record not found.");
|
||||
}
|
||||
|
||||
$product_sku = $from_row['product_sku'];
|
||||
$product_name = $this->getProductName($product_sku);
|
||||
$to_warehouse = (int)$from_row['ref_warehouse'];
|
||||
$ref_id = (int)$from_row['ref_id'];
|
||||
|
||||
// Fetch the "to" row (transfer_in side)
|
||||
$to_ctx = $this->getStockContext($to_warehouse, $ref_id);
|
||||
$to_row = $to_ctx['row'];
|
||||
|
||||
if (!$to_row) {
|
||||
throw new Exception("Paired destination record missing — data integrity issue.");
|
||||
}
|
||||
|
||||
// Single global validation covers both warehouses
|
||||
$this->validateLatestTransaction($product_sku, $from_row['date'], $product_name);
|
||||
|
||||
// Soft-delete both rows
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `$from_table`
|
||||
SET company_id = company_id * -1
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([':id' => $from_stock_id, ':company_id' => $this->company_id]);
|
||||
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `{$to_ctx['table']}`
|
||||
SET company_id = company_id * -1
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([':id' => $ref_id, ':company_id' => $this->company_id]);
|
||||
|
||||
// Destination rack was occupied by transfer_in — release it
|
||||
$this->releaseRack(
|
||||
$to_warehouse,
|
||||
$to_row['zone'], $to_row['aisle'], $to_row['rack']
|
||||
);
|
||||
|
||||
// Source rack was released by transfer_out — re-occupy with original batch
|
||||
$this->occupyRack(
|
||||
$from_warehouse_id,
|
||||
$from_row['zone'], $from_row['aisle'], $from_row['rack'],
|
||||
$product_sku,
|
||||
$from_stock_id
|
||||
);
|
||||
|
||||
// Reverse balances
|
||||
$quantity = (int)$from_row['out'];
|
||||
$this->adjustBalance('out', $from_warehouse_id, $product_sku, $quantity, 0);
|
||||
$this->adjustBalance('in', $to_warehouse, $product_sku, $quantity, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete a warehouse.
|
||||
*
|
||||
* Blocks if any md_storage references this warehouse by name.
|
||||
*
|
||||
* Must be called inside a DB transaction by the caller.
|
||||
*
|
||||
* @throws Exception if warehouse not found or has dependent storage
|
||||
*/
|
||||
public function deleteWarehouse(int $warehouse_id): void {
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, warehouse_name, `log` FROM md_warehouse
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':id' => $warehouse_id,
|
||||
]);
|
||||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
throw new Exception("Warehouse not found.");
|
||||
}
|
||||
|
||||
// Block if any md_storage references this warehouse by name
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM md_storage
|
||||
WHERE company_id = :company_id
|
||||
AND warehouse = :warehouse_name"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':warehouse_name' => $row['warehouse_name'],
|
||||
]);
|
||||
|
||||
if ($sth->fetchColumn() > 0) {
|
||||
throw new Exception(
|
||||
"Cannot delete — warehouse \"{$row['warehouse_name']}\" " .
|
||||
"still has storage locations assigned to it."
|
||||
);
|
||||
}
|
||||
|
||||
// Append delete event to log
|
||||
$log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$log[] = $this->buildLogEntry('delete');
|
||||
|
||||
// Soft-delete
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_warehouse
|
||||
SET company_id = company_id * -1,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':log' => json_encode($log),
|
||||
':id' => $warehouse_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Soft-delete a storage record and its associated racks.
|
||||
*
|
||||
* - Blocks if any md_rack under this storage_id is occupied
|
||||
* - Hard-deletes md_rack_log rows for those racks
|
||||
* - Soft-deletes all md_rack rows under this storage_id
|
||||
* - Soft-deletes md_storage
|
||||
*
|
||||
* Must be called inside a DB transaction by the caller.
|
||||
*
|
||||
* @throws Exception if storage not found or has occupied racks
|
||||
*/
|
||||
public function deleteStorage(int $storage_id): void {
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, zone, `log` FROM md_storage
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':id' => $storage_id,
|
||||
]);
|
||||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
throw new Exception("Storage not found.");
|
||||
}
|
||||
|
||||
// Block if any rack under this storage is occupied
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM md_rack
|
||||
WHERE company_id = :company_id
|
||||
AND storage_id = :storage_id
|
||||
AND product_sku IS NOT NULL"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':storage_id' => $storage_id,
|
||||
]);
|
||||
|
||||
if ($sth->fetchColumn() > 0) {
|
||||
throw new Exception(
|
||||
"Cannot delete — storage zone \"{$row['zone']}\" " .
|
||||
"still has occupied racks with stock."
|
||||
);
|
||||
}
|
||||
|
||||
// Collect rack IDs before deleting — needed for rack log cleanup
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id FROM md_rack
|
||||
WHERE company_id = :company_id
|
||||
AND storage_id = :storage_id"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':storage_id' => $storage_id,
|
||||
]);
|
||||
$rack_ids = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Hard-delete md_rack_log for these racks
|
||||
if (!empty($rack_ids)) {
|
||||
$placeholders = implode(',', array_fill(0, count($rack_ids), '?'));
|
||||
$this->pdo->prepare(
|
||||
"DELETE FROM md_rack_log WHERE md_rack_id IN ($placeholders)"
|
||||
)->execute($rack_ids);
|
||||
}
|
||||
|
||||
// Soft-delete all md_rack rows under this storage
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_rack
|
||||
SET company_id = company_id * -1
|
||||
WHERE company_id = :company_id
|
||||
AND storage_id = :storage_id"
|
||||
)->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':storage_id' => $storage_id,
|
||||
]);
|
||||
|
||||
// Append delete event to log
|
||||
$log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$log[] = $this->buildLogEntry('delete');
|
||||
|
||||
// Soft-delete md_storage
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_storage
|
||||
SET company_id = company_id * -1,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':log' => json_encode($log),
|
||||
':id' => $storage_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ContactManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
|
||||
|
||||
$contactMgmt = new ContactManager($pdo, $company_id);
|
||||
$contactMgmt->deleteContact((int)$data['id']);
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ContactManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
|
||||
|
||||
$contactMgmt = new ContactManager($pdo, $company_id);
|
||||
$contactMgmt->deleteContactType((int)$data['id']);
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
+44
-2
@@ -268,7 +268,7 @@
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>contact/manage_contact_type.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
<a href="javascript:;" class="link-danger" onclick="remove_contact_type(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
@@ -331,7 +331,7 @@
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>contact/manage_contact.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
<a href="javascript:;" class="link-danger" onclick="remove_contact(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
@@ -340,6 +340,48 @@
|
||||
|
||||
}
|
||||
|
||||
|
||||
function remove_contact_type(id,element) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>contact/api/engine/remove_contact_type.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
debugMode: 0,
|
||||
data: {
|
||||
id: id
|
||||
},
|
||||
action: 'delete',
|
||||
onSuccess: function(res) {
|
||||
|
||||
element.parent().parent().remove();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
function remove_contact(id,element) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>contact/api/engine/remove_contact.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
debugMode: 0,
|
||||
data: {
|
||||
id: id
|
||||
},
|
||||
action: 'delete',
|
||||
onSuccess: function(res) {
|
||||
|
||||
element.parent().parent().remove();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(async function() {
|
||||
try {
|
||||
await retrieve_contact_type();
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
|
||||
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
$whMgmt->deleteStockIn(
|
||||
(int)$data['id'],
|
||||
(int)$data['wh']
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
|
||||
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
$whMgmt->deleteStockOut(
|
||||
(int)$data['id'],
|
||||
(int)$data['wh']
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
|
||||
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
$whMgmt->deleteTransfer(
|
||||
(int)$data['id'],
|
||||
(int)$data['wh']
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -24,11 +24,6 @@
|
||||
|
||||
// UPDATE existing stock_in record
|
||||
$sql = "UPDATE `$table` SET
|
||||
`product_sku` = :product_sku,
|
||||
`in` = :quantity,
|
||||
`zone` = :zone,
|
||||
`aisle` = :aisle,
|
||||
`rack` = :rack,
|
||||
`contact_id` = :contact_id,
|
||||
`description` = :description,
|
||||
`log` = :log
|
||||
@@ -38,41 +33,12 @@
|
||||
$sth->execute([
|
||||
":id" => $id,
|
||||
":company_id" => $company_id,
|
||||
":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),
|
||||
]);
|
||||
|
||||
// On UPDATE, td_stock_id stays the same (this record's ID)
|
||||
$td_stock_id = $id;
|
||||
|
||||
// Only touch md_rack if the physical location actually changed.
|
||||
// Skipping when unchanged avoids redundant log entries and release/occupy cycles.
|
||||
$rack_changed = (
|
||||
($row['zone'] ?? '') != $data['zone'] ||
|
||||
($row['aisle'] ?? '') != $data['aisle'] ||
|
||||
($row['rack'] ?? '') != $data['rack']
|
||||
);
|
||||
|
||||
if ($rack_changed) {
|
||||
// Free up the old rack
|
||||
$whMgmt->releaseRack(
|
||||
$warehouse_id,
|
||||
$row['zone'], $row['aisle'], $row['rack']
|
||||
);
|
||||
// Claim the new rack, linked to the same stock record
|
||||
$whMgmt->occupyRack(
|
||||
$warehouse_id,
|
||||
$data['zone'], $data['aisle'], $data['rack'],
|
||||
$data['product_sku'],
|
||||
$td_stock_id
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
@@ -107,8 +73,6 @@
|
||||
$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(
|
||||
@@ -119,6 +83,8 @@
|
||||
$data["quantity"]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
@@ -19,6 +19,26 @@
|
||||
$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(
|
||||
@@ -46,60 +66,6 @@
|
||||
$quantity = (int) $source_stock['in'];
|
||||
$ref_id = (int) $source_stock['id'];
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
// UPDATE existing stock_out record
|
||||
$sql = "UPDATE `$table` SET
|
||||
`product_sku` = :product_sku,
|
||||
`out` = :quantity,
|
||||
`zone` = :zone,
|
||||
`aisle` = :aisle,
|
||||
`rack` = :rack,
|
||||
`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,
|
||||
":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),
|
||||
]);
|
||||
|
||||
// Only touch md_rack if the physical location actually changed.
|
||||
$rack_changed = (
|
||||
($row['zone'] ?? '') != $data['zone'] ||
|
||||
($row['aisle'] ?? '') != $data['aisle'] ||
|
||||
($row['rack'] ?? '') != $data['rack']
|
||||
);
|
||||
|
||||
if ($rack_changed) {
|
||||
// Re-occupy the old rack with the batch it originally held
|
||||
// (logically: the stock goes back to where it came from)
|
||||
$whMgmt->occupyRack(
|
||||
$warehouse_id,
|
||||
$row['zone'], $row['aisle'], $row['rack'],
|
||||
$row['product_sku'],
|
||||
$row['ref_id'] ?? null
|
||||
);
|
||||
|
||||
// Release the new rack (the real stock-out action)
|
||||
$whMgmt->releaseRack(
|
||||
$warehouse_id,
|
||||
$data['zone'], $data['aisle'], $data['rack']
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// INSERT new stock_out record.
|
||||
// ref_id links this stock_out row back to the stock_in batch it consumed.
|
||||
$sql = "INSERT INTO `$table`
|
||||
@@ -130,8 +96,6 @@
|
||||
$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
|
||||
@@ -143,6 +107,8 @@
|
||||
$quantity
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
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.company_id = c.company_id and
|
||||
a.product_sku = c.sku
|
||||
WHERE
|
||||
a.company_id = :company_id and
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
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.company_id = c.company_id and
|
||||
a.product_sku = c.sku
|
||||
WHERE
|
||||
a.company_id = :company_id and
|
||||
|
||||
@@ -273,6 +273,8 @@
|
||||
$('#product_name').val(data.product_name);
|
||||
$('#product_sku').attr('secondary', data.product_sku);
|
||||
|
||||
$('#product_sku,#quantity').prop('disabled', true);
|
||||
|
||||
$(`button[type=submit]`).text('Update');
|
||||
$(`button[type=reset]`).hide();
|
||||
|
||||
|
||||
@@ -271,6 +271,8 @@
|
||||
$('#product_name').val(data.product_name);
|
||||
$('#product_sku').attr('secondary', data.product_sku);
|
||||
|
||||
$('#product_sku,#quantity').prop('disabled', true);
|
||||
|
||||
$(`button[type=submit]`).text('Update');
|
||||
$(`button[type=reset]`).hide();
|
||||
|
||||
|
||||
@@ -344,10 +344,12 @@
|
||||
$('#contact').val(data.contact_name)
|
||||
.attr('secondary', data.contact_name)
|
||||
.attr('data-id', data.contact_id);
|
||||
console.log(data);
|
||||
|
||||
$('#product_name').val(data.product_name);
|
||||
$('#product_sku').attr('secondary', data.product_sku);
|
||||
|
||||
$('#product_sku,#quantity').prop('disabled', true);
|
||||
|
||||
$(`button[type=submit]`).text('Update');
|
||||
$(`button[type=reset]`).hide();
|
||||
|
||||
|
||||
+22
-1
@@ -133,7 +133,10 @@
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>ics/manage_stock_in.php?id=${item['id']}&wh=${warehouse}" class=""><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
<a href="javascript:void(0);" class="link-danger"
|
||||
onclick="delete_stock_in($(this),${item['id']})">
|
||||
<i class="ti ti-trash ms-2 fs-5"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
@@ -162,6 +165,24 @@
|
||||
|
||||
}
|
||||
|
||||
function delete_stock_in(element, id) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/delete_stock_in.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'delete',
|
||||
data : {
|
||||
id: id,
|
||||
wh: $(`select#warehouse`).val()
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
element.closest('tr').remove();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(async function() {
|
||||
try {
|
||||
await retrieve_warehouse();
|
||||
|
||||
+22
-1
@@ -133,7 +133,10 @@
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>ics/manage_stock_out.php?id=${item['id']}&wh=${warehouse}" class=""><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
<a href="javascript:void(0);" class="link-danger"
|
||||
onclick="delete_stock_out($(this),${item['id']})">
|
||||
<i class="ti ti-trash ms-2 fs-5"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
@@ -162,6 +165,24 @@
|
||||
|
||||
}
|
||||
|
||||
function delete_stock_out(element, id) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/delete_stock_out.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'delete',
|
||||
data : {
|
||||
id: id,
|
||||
wh: $(`select#warehouse`).val()
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
element.closest('tr').remove();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(async function() {
|
||||
try {
|
||||
await retrieve_warehouse();
|
||||
|
||||
@@ -133,7 +133,10 @@
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>ics/manage_stock_transfer.php?id=${item['id']}&wh=${warehouse}" class=""><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
<a href="javascript:void(0);" class="link-danger"
|
||||
onclick="delete_stock_transfer($(this),${item['id']})">
|
||||
<i class="ti ti-trash ms-2 fs-5"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
@@ -162,6 +165,26 @@
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function delete_stock_transfer(element, id) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/delete_stock_transfer.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'delete',
|
||||
data : {
|
||||
id: id,
|
||||
wh: $(`select#warehouse`).val()
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
element.closest('tr').remove();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(async function() {
|
||||
try {
|
||||
await retrieve_warehouse();
|
||||
|
||||
@@ -1,69 +1,18 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
|
||||
try {
|
||||
$result = dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) {
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
|
||||
|
||||
if ($id <= 0) {
|
||||
throw new Exception("Invalid category ID.");
|
||||
}
|
||||
$productMgmt = new ProductManager($pdo, $company_id);
|
||||
$productMgmt->deleteCategory((int)$data['id']);
|
||||
|
||||
// 1. Validate: check if category is in use by any product
|
||||
$sql = "SELECT COUNT(*)
|
||||
FROM md_product
|
||||
WHERE company_id = :company_id
|
||||
AND category = :id";
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id
|
||||
]);
|
||||
|
||||
$product_count = (int) $sth->fetchColumn();
|
||||
|
||||
if ($product_count > 0) {
|
||||
throw new Exception("Cannot remove category. It is currently used by {$product_count} product(s).");
|
||||
}
|
||||
|
||||
// 2. Append to log history
|
||||
$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);
|
||||
if (!is_array($table_log)) $table_log = [];
|
||||
$table_log[] = $logging;
|
||||
|
||||
// 3. Soft-delete via sign flip
|
||||
$sql = "UPDATE md_product_category
|
||||
SET company_id = company_id * (-1),
|
||||
`log` = :log
|
||||
WHERE company_id = :company_id
|
||||
AND id = :id";
|
||||
$sth = $pdo->prepare($sql);
|
||||
$sth->execute([
|
||||
":company_id" => $company_id,
|
||||
":id" => $id,
|
||||
":log" => json_encode($table_log, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
|
||||
if ($sth->rowCount() === 0) {
|
||||
throw new Exception("Category not found or already removed.");
|
||||
}
|
||||
|
||||
return $id;
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer["message"] = "Category removed successfully.";
|
||||
$answer["id"] = $result;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
|
||||
|
||||
$productMgmt = new ProductManager($pdo, $company_id);
|
||||
$productMgmt->deleteProduct((int)$data['id']);
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
|
||||
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
$whMgmt->deleteStorage((int)$data['id']);
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
try {
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
|
||||
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
$whMgmt->deleteWarehouse((int)$data['id']);
|
||||
|
||||
});
|
||||
|
||||
$answer["success"] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = "Database error, please try again";
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer["success"] = 0;
|
||||
$answer["message"] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -298,6 +298,28 @@
|
||||
|
||||
|
||||
|
||||
function remove_product(id,element) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>inventory/api/engine/remove_product.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
debugMode: 0,
|
||||
data: {
|
||||
id: id
|
||||
},
|
||||
action: 'delete',
|
||||
onSuccess: function(res) {
|
||||
|
||||
element.parent().parent().remove();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function change_page_category(page_num) {
|
||||
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
@@ -382,7 +404,7 @@
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>inventory/manage_product.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
<a href="javascript:;" class="link-danger" onclick="remove_product(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
|
||||
@@ -335,7 +335,7 @@
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>inventory/manage_warehouse.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
<a href="javascript:;" class="link-danger" onclick="remove_warehouse(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
@@ -393,7 +393,7 @@
|
||||
<td class="py-3"></td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>inventory/manage_storage.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
<a href="javascript:;" class="link-danger" onclick="remove_storage(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
@@ -402,6 +402,49 @@
|
||||
|
||||
}
|
||||
|
||||
|
||||
function remove_warehouse(id,element) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>inventory/api/engine/remove_warehouse.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
debugMode: 0,
|
||||
data: {
|
||||
id: id
|
||||
},
|
||||
action: 'delete',
|
||||
onSuccess: function(res) {
|
||||
|
||||
element.parent().parent().remove();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
function remove_storage(id,element) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>inventory/api/engine/remove_storage.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
debugMode: 0,
|
||||
data: {
|
||||
id: id
|
||||
},
|
||||
action: 'delete',
|
||||
onSuccess: function(res) {
|
||||
|
||||
element.parent().parent().remove();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
$(async function() {
|
||||
try {
|
||||
await retrieve_stats();
|
||||
|
||||
Reference in New Issue
Block a user