$data * @return int|null Inserted ID or null on failure */ public function insert(int $tenantId, array $data): ?int { $result = DB::insert( 'INSERT INTO security_checks' . ' (tenant_id, debitor_no, debitor_name, domain_no, domain_url, product_code, product_name,' . ' template_id, owner_user_id, status, process_state_json, tech_schema_snapshot_json, tech_state_json, created_by)' . ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (string) $tenantId, (string) ($data['debitor_no'] ?? ''), (string) ($data['debitor_name'] ?? ''), (string) ($data['domain_no'] ?? ''), (string) ($data['domain_url'] ?? ''), (string) ($data['product_code'] ?? ''), (string) ($data['product_name'] ?? ''), isset($data['template_id']) ? (string) ((int) $data['template_id']) : null, isset($data['owner_user_id']) ? (string) ((int) $data['owner_user_id']) : null, (string) ($data['status'] ?? 'open'), $data['process_state_json'] ?? null, $data['tech_schema_snapshot_json'] ?? null, $data['tech_state_json'] ?? null, (string) ((int) ($data['created_by'] ?? 0)) ); return $result === false ? null : (int) $result; } public function findById(int $tenantId, int $id): ?array { if ($id <= 0) { return null; } $row = DB::selectOne( 'SELECT c.*, uo.display_name AS owner_name, uc.display_name AS created_by_name, uu.display_name AS updated_by_name' . ' FROM security_checks c' . ' LEFT JOIN users uo ON uo.id = c.owner_user_id' . ' LEFT JOIN users uc ON uc.id = c.created_by' . ' LEFT JOIN users uu ON uu.id = c.updated_by' . ' WHERE c.id = ? AND c.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'] ?? '')); $view = trim((string) ($filters['view'] ?? '')); [$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 100, 0); [$order, $dir] = RepoQuery::sanitizeOrder( $filters, ['id', 'debitor_name', 'domain_url', 'product_name', 'status', 'created_at', 'updated_at'], 'created_at', 'desc' ); $where = ['c.tenant_id = ?']; $params = [(string) $tenantId]; RepoQuery::addLikeFilter($where, $params, ['c.debitor_name', 'c.debitor_no', 'c.domain_url', 'c.product_name'], $search); if ($status !== '' && $status !== 'all') { $where[] = 'c.status = ?'; $params[] = $status; } elseif ($view === 'active') { $where[] = "c.status IN ('open', 'in_progress')"; } elseif ($view === 'done') { $where[] = "c.status IN ('completed', 'archived')"; } $whereSql = ' WHERE ' . implode(' AND ', $where); $total = (int) (DB::selectValue('SELECT COUNT(*) FROM security_checks c' . $whereSql, ...$params) ?? 0); $rows = DB::select( 'SELECT c.*, uo.display_name AS owner_name, uc.display_name AS created_by_name' . ' FROM security_checks c' . ' LEFT JOIN users uo ON uo.id = c.owner_user_id' . ' LEFT JOIN users uc ON uc.id = c.created_by' . $whereSql . sprintf(' ORDER BY c.`%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]; } /** * Active users in the tenant, for owner assignment. Ordered by display name. * * @return list */ public function listTenantUsers(int $tenantId): array { $rows = DB::select( 'SELECT u.id, u.display_name FROM users u' . ' JOIN user_tenants ut ON ut.user_id = u.id' . ' WHERE ut.tenant_id = ? AND u.active = 1' . ' ORDER BY u.display_name ASC', (string) $tenantId ); $users = []; if (is_array($rows)) { foreach ($rows as $row) { $flat = $this->normalizeRow($row); if ($flat === null) { continue; } $id = (int) ($flat['id'] ?? 0); if ($id > 0) { $users[] = ['id' => $id, 'display_name' => (string) ($flat['display_name'] ?? '')]; } } } return $users; } /** * @param array $data */ public function updateState(int $tenantId, int $id, array $data): bool { $result = DB::update( 'UPDATE security_checks SET process_state_json = ?, tech_state_json = ?, status = ?, updated_by = ?' . ' WHERE id = ? AND tenant_id = ?', $data['process_state_json'] ?? null, $data['tech_state_json'] ?? null, (string) ($data['status'] ?? 'open'), (string) ((int) ($data['updated_by'] ?? 0)), (string) $id, (string) $tenantId ); return $result !== false; } public function updateStatus(int $tenantId, int $id, string $status, int $updatedBy): bool { $result = DB::update( 'UPDATE security_checks SET status = ?, updated_by = ? WHERE id = ? AND tenant_id = ?', $status, (string) $updatedBy, (string) $id, (string) $tenantId ); return $result !== false; } public function deleteById(int $tenantId, int $id): bool { if ($id <= 0) { return false; } return (bool) DB::delete( 'DELETE FROM security_checks WHERE id = ? AND tenant_id = ?', (string) $id, (string) $tenantId ); } private function normalizeRow(mixed $row): ?array { if (!is_array($row)) { return null; } $item = []; foreach ($row as $key => $value) { if (is_array($value)) { $item = array_merge($item, $value); } else { $item[$key] = $value; } } return isset($item['id']) ? $item : null; } }