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, ]); } // ───────────────────────────────────────────────────────────── // 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); } } }