$data * @return int|null Inserted ID or null on failure */ public function insert(array $data): ?int { $result = DB::insert( 'INSERT INTO helpdesk_handovers ' . '(tenant_id, debitor_no, debitor_name, product_code, domain_no, domain_url, status, schema_snapshot, field_values, created_by) ' . 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (string) ($data['tenant_id'] ?? 0), (string) ($data['debitor_no'] ?? ''), (string) ($data['debitor_name'] ?? ''), (string) ($data['product_code'] ?? ''), (string) ($data['domain_no'] ?? ''), (string) ($data['domain_url'] ?? ''), (string) ($data['status'] ?? 'draft'), $data['schema_snapshot'] ?? null, $data['field_values'] ?? null, (string) ($data['created_by'] ?? 0) ); if ($result === false) { return null; } return (int) $result; } public function findById(int $tenantId, int $id): ?array { if ($id <= 0) { return null; } $row = DB::selectOne( 'SELECT h.*,' . ' uc.display_name AS created_by_label, uc.uuid AS created_by_uuid,' . ' uu.display_name AS updated_by_label, uu.uuid AS updated_by_uuid,' . ' ua.display_name AS assigned_to_label, ua.uuid AS assigned_to_uuid, ua.email AS assigned_to_email,' . ' ub.display_name AS assigned_by_label, ub.uuid AS assigned_by_uuid, ub.email AS assigned_by_email' . ' FROM helpdesk_handovers h' . ' LEFT JOIN users uc ON uc.id = h.created_by' . ' LEFT JOIN users uu ON uu.id = h.updated_by' . ' LEFT JOIN users ua ON ua.id = h.assigned_to' . ' LEFT JOIN users ub ON ub.id = h.assigned_by' . ' WHERE h.id = ? AND h.tenant_id = ? LIMIT 1', (string) $id, (string) $tenantId ); return $this->normalizeRow($row); } /** * @param array $filters * @return array{total: int, rows: list>} */ public function listPaged(int $tenantId, array $filters): array { $search = trim((string) ($filters['search'] ?? '')); $status = trim((string) ($filters['status'] ?? '')); $productCode = trim((string) ($filters['product_code'] ?? '')); $assignedTo = (int) ($filters['assigned_to'] ?? 0); $view = trim((string) ($filters['view'] ?? '')); [$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 100, 0); [$order, $dir] = RepoQuery::sanitizeOrder( $filters, ['id', 'domain_url', 'debitor_name', 'product_code', 'status', 'created_at', 'created_by', 'assigned_at', 'due_date'], 'created_at', 'desc' ); $where = ['h.tenant_id = ?']; $params = [(string) $tenantId]; RepoQuery::addLikeFilter($where, $params, ['h.debitor_name', 'h.debitor_no', 'h.domain_url', 'h.product_code'], $search); if ($status !== '' && $status !== 'all') { $where[] = 'h.status = ?'; $params[] = $status; } elseif ($view === 'open') { $where[] = "h.status NOT IN ('completed', 'archived')"; } elseif ($view === 'closed') { $where[] = "h.status IN ('completed', 'archived')"; } if ($productCode !== '') { $where[] = 'h.product_code = ?'; $params[] = $productCode; } if ($assignedTo > 0) { $where[] = 'h.assigned_to = ?'; $params[] = (string) $assignedTo; } $whereSql = ' WHERE ' . implode(' AND ', $where); $total = (int) (DB::selectValue('SELECT COUNT(*) FROM helpdesk_handovers h' . $whereSql, ...$params) ?? 0); $rows = DB::select( 'SELECT h.*, u.display_name AS created_by_name,' . ' ua.display_name AS assigned_to_name,' . ' sp.name AS product_name, sp.bc_description AS product_bc_description' . ' FROM helpdesk_handovers h' . ' LEFT JOIN users u ON u.id = h.created_by' . ' LEFT JOIN users ua ON ua.id = h.assigned_to' . ' LEFT JOIN helpdesk_software_products sp ON sp.code = h.product_code' . $whereSql . sprintf(' ORDER BY h.`%s` %s LIMIT ? OFFSET ?', $order, $dir), ...array_merge($params, [(string) $limit, (string) $offset]) ); $normalized = []; if (is_array($rows)) { foreach ($rows as $row) { $item = $this->normalizeRow($row); if ($item !== null) { $normalized[] = $item; } } } return [ 'total' => $total, 'rows' => $normalized, ]; } public function updateFieldValues(int $tenantId, int $id, string $fieldValuesJson, int $updatedBy): bool { $result = DB::update( 'UPDATE helpdesk_handovers SET field_values = ?, updated_by = ? WHERE id = ? AND tenant_id = ?', $fieldValuesJson, (string) $updatedBy, (string) $id, (string) $tenantId ); return $result !== false; } public function updateStatus(int $tenantId, int $id, string $status, int $updatedBy): bool { $result = DB::update( 'UPDATE helpdesk_handovers SET status = ?, updated_by = ? WHERE id = ? AND tenant_id = ?', $status, (string) $updatedBy, (string) $id, (string) $tenantId ); return $result !== false; } public function assign(int $tenantId, int $id, int $assignedTo, int $assignedBy, ?string $dueDate, int $updatedBy): bool { $result = DB::update( 'UPDATE helpdesk_handovers' . ' SET assigned_to = ?, assigned_by = ?, assigned_at = NOW(), due_date = ?, updated_by = ?' . ' WHERE id = ? AND tenant_id = ?', (string) $assignedTo, (string) $assignedBy, ($dueDate !== null && $dueDate !== '') ? $dueDate : null, (string) $updatedBy, (string) $id, (string) $tenantId ); return $result !== false; } /** * @param list $ids */ public function deleteByIds(int $tenantId, array $ids): int { if ($ids === []) { return 0; } $placeholders = implode(',', array_fill(0, count($ids), '?')); $params = array_map('strval', $ids); $params[] = (string) $tenantId; $result = DB::update( 'DELETE FROM helpdesk_handovers WHERE id IN (' . $placeholders . ') AND tenant_id = ?', ...$params ); return is_int($result) ? $result : 0; } /** * Find all handovers linked to a specific domain. * * @api Called from DomainDetailService * @return list> */ public function findByDomainNo(int $tenantId, string $domainNo): array { if ($domainNo === '') { return []; } $rows = DB::select( 'SELECT h.*, u.display_name AS created_by_name,' . ' sp.name AS product_name, sp.bc_description AS product_bc_description' . ' FROM helpdesk_handovers h' . ' LEFT JOIN users u ON u.id = h.created_by' . ' LEFT JOIN helpdesk_software_products sp ON sp.code = h.product_code' . ' WHERE h.tenant_id = ? AND h.domain_no = ?' . ' ORDER BY h.created_at DESC', (string) $tenantId, $domainNo ); $normalized = []; if (is_array($rows)) { foreach ($rows as $row) { $item = $this->normalizeRow($row); if ($item !== null) { $normalized[] = $item; } } } return $normalized; } public function beginTransaction(): void { DB::handle()->begin_transaction(); } public function commitTransaction(): void { DB::handle()->commit(); } public function rollbackTransaction(): void { DB::handle()->rollback(); } private function normalizeRow(mixed $row): ?array { if (!is_array($row)) { return null; } // JOIN queries return nested arrays keyed by table name — merge all sub-arrays $item = []; foreach ($row as $key => $value) { if (is_array($value)) { $item = array_merge($item, $value); } else { $item[$key] = $value; } } if (!isset($item['id'])) { return null; } return $item; } }