modify classed and comments
This commit is contained in:
@@ -3,11 +3,18 @@
|
||||
/**
|
||||
* ContactManager
|
||||
*
|
||||
* Encapsulates contact and contact type operations:
|
||||
* - Soft-delete with downstream validation
|
||||
* Handles all CRUD and soft-delete operations for contacts and contact types.
|
||||
*
|
||||
* Note: Methods that modify data do NOT manage their own DB transactions.
|
||||
* Callers are responsible for wrapping operations in dbTransaction() when atomicity is needed.
|
||||
* Method order:
|
||||
* Master file basis → get/save/delete for contact types and contacts
|
||||
* Transaction basis → (none — contacts are master data only)
|
||||
* Report basis → (none — reporting is handled by ReportManager)
|
||||
*
|
||||
* Note: Write methods do NOT manage their own DB transactions.
|
||||
* Callers must wrap multi-step operations inside dbTransaction().
|
||||
*
|
||||
* Security: All SQL uses PDO prepared statements with bound parameters.
|
||||
* No user input is ever interpolated directly into a query string.
|
||||
*/
|
||||
class ContactManager {
|
||||
|
||||
@@ -24,8 +31,15 @@ class ContactManager {
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Check if a contact_id is referenced in any active td_stock_* row.
|
||||
* Returns true if blocking stock exists.
|
||||
* Check whether a contact is referenced by any active stock transaction
|
||||
* across all td_stock_* warehouse tables.
|
||||
*
|
||||
* Used as a pre-delete guard to prevent orphaning stock records.
|
||||
* Scans information_schema to discover all td_stock_* tables dynamically.
|
||||
* Table names from information_schema are backtick-quoted for safety.
|
||||
*
|
||||
* @param int $contact_id The md_contact.id to check.
|
||||
* @return bool true if at least one stock row references this contact, false if clear.
|
||||
*/
|
||||
private function hasActiveStock(int $contact_id): bool {
|
||||
|
||||
@@ -38,6 +52,8 @@ class ContactManager {
|
||||
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
// table name comes from information_schema (trusted system table),
|
||||
// and is backtick-quoted — no user input reaches the table identifier.
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM `$table`
|
||||
WHERE company_id = :company_id
|
||||
@@ -56,7 +72,17 @@ class ContactManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a log entry array for audit context.
|
||||
* Build a standard audit log entry array for append-to-JSON log columns.
|
||||
*
|
||||
* Captures the acting user_id, current datetime, session login time,
|
||||
* and a short action label (e.g. 'delete', 'update').
|
||||
*
|
||||
* Usage:
|
||||
* $log[] = $this->buildLogEntry('delete');
|
||||
* $params[':log'] = json_encode($log);
|
||||
*
|
||||
* @param string $action Short label describing the operation (e.g. 'delete').
|
||||
* @return array Associative array ready to be appended to a log array.
|
||||
*/
|
||||
private function buildLogEntry(string $action): array {
|
||||
return [
|
||||
@@ -70,126 +96,15 @@ class ContactManager {
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Contact type
|
||||
// MASTER FILE BASIS — Contact Type
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Soft-delete a contact type.
|
||||
* Return all contact types for the company.
|
||||
*
|
||||
* Blocks if any md_contact references this type.
|
||||
* Used to populate dropdowns and the contact type listing page.
|
||||
*
|
||||
* @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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Contact type queries
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Full contact type list.
|
||||
* @return array All rows from md_contact_type for this company.
|
||||
*/
|
||||
public function getContactTypeList(): array
|
||||
{
|
||||
@@ -202,7 +117,12 @@ class ContactManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single contact type row by ID.
|
||||
* Fetch a single contact type row by its primary key.
|
||||
*
|
||||
* Used to pre-fill the edit form on the manage contact type page.
|
||||
*
|
||||
* @param int $id The md_contact_type.id to fetch.
|
||||
* @return array|false Associative row, or false if not found.
|
||||
*/
|
||||
public function getContactTypeById(int $id): array|false
|
||||
{
|
||||
@@ -215,9 +135,16 @@ class ContactManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a contact type.
|
||||
* Pass $data['id'] > 0 for update, 0 for insert.
|
||||
* Insert a new contact type or update an existing one.
|
||||
*
|
||||
* Pass $data['id'] = 0 to insert; pass $data['id'] > 0 to update.
|
||||
* The $logging array is appended to the row's JSON log column
|
||||
* (caller constructs this from session/request context).
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param array $data Keys: id, contact_type, description, status.
|
||||
* @param array $logging Audit entry to append to the log column.
|
||||
*/
|
||||
public function saveContactType(array $data, array $logging): void
|
||||
{
|
||||
@@ -259,12 +186,78 @@ class ContactManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete a contact type by negating its company_id.
|
||||
*
|
||||
* Blocks deletion if any md_contact row is still assigned to this type,
|
||||
* preventing orphaned contacts.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param int $type_id The md_contact_type.id to delete.
|
||||
* @throws Exception If the type is not found or has contacts assigned to it.
|
||||
*/
|
||||
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: negate company_id so row is hidden but recoverable
|
||||
$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 queries
|
||||
// MASTER FILE BASIS — Contact
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Full contact list.
|
||||
* Return all contacts for the company.
|
||||
*
|
||||
* Used to populate the contact listing page and bulk dropdowns.
|
||||
*
|
||||
* @return array All rows from md_contact for this company.
|
||||
*/
|
||||
public function getContactList(): array
|
||||
{
|
||||
@@ -277,7 +270,12 @@ class ContactManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single contact row by ID.
|
||||
* Fetch a single contact row by its primary key.
|
||||
*
|
||||
* Used to pre-fill the edit form on the manage contact page.
|
||||
*
|
||||
* @param int $id The md_contact.id to fetch.
|
||||
* @return array|false Associative row, or false if not found.
|
||||
*/
|
||||
public function getContactById(int $id): array|false
|
||||
{
|
||||
@@ -290,7 +288,13 @@ class ContactManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search contacts by name keyword — for autocomplete.
|
||||
* Search contacts by name keyword — for live autocomplete on stock forms.
|
||||
*
|
||||
* Returns up to 50 matches. The keyword is safely bound as a LIKE parameter;
|
||||
* no wildcard escaping is needed here since '%' wrapping is the intended behaviour.
|
||||
*
|
||||
* @param string $keyword Partial contact name to match.
|
||||
* @return array Matching md_contact rows.
|
||||
*/
|
||||
public function searchContact(string $keyword): array
|
||||
{
|
||||
@@ -307,11 +311,21 @@ class ContactManager {
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert or update a contact.
|
||||
* Pass $data['id'] > 0 for update, 0 for insert.
|
||||
* Insert a new contact or update an existing one.
|
||||
*
|
||||
* Pass $data['id'] = 0 to insert; pass $data['id'] > 0 to update.
|
||||
* $contact_image is the resolved filename/path from FileUploader — may be
|
||||
* the existing image when no new file was uploaded.
|
||||
* The $logging array is appended to the row's JSON log column.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param array $data Keys: id, contact_name, tax_id, organization, branch,
|
||||
* contact_type, billing_address, shipping_location,
|
||||
* shipping_address, remark, status.
|
||||
* @param array $logging Audit entry to append to the log column.
|
||||
* @param string $contact_image Stored filename for the contact's profile image.
|
||||
*/
|
||||
public function saveContact(array $data, array $logging, string $contact_image): void
|
||||
{
|
||||
@@ -370,4 +384,57 @@ class ContactManager {
|
||||
)->execute($params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete a contact by negating its company_id.
|
||||
*
|
||||
* Blocks deletion if the contact is referenced in any active stock
|
||||
* transaction across all td_stock_* warehouse tables, preventing
|
||||
* broken foreign key references in transaction history.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param int $contact_id The md_contact.id to delete.
|
||||
* @throws Exception If the contact is not found or has active stock references.
|
||||
*/
|
||||
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: negate company_id so row is hidden but recoverable
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user