value), $data['source_filename'] ?? null, $data['mapped_targets_csv'] ?? null, $data['user_id'] !== null ? (string) ((int) $data['user_id']) : null, $data['current_tenant_id'] !== null ? (string) ((int) $data['current_tenant_id']) : null ); return $id ? (int) $id : false; } public function finishById(int $id, array $data): bool { if ($id <= 0) { return false; } $updated = DB::update( 'update import_audit_runs set status = ?, rows_total = ?, created_count = ?, skipped_count = ?, failed_count = ?, error_codes_json = ?, duration_ms = ?, finished_at = NOW() where id = ?', (string) ($data['status'] ?? ImportAuditStatus::Failed->value), (string) ((int) ($data['rows_total'] ?? 0)), (string) ((int) ($data['created_count'] ?? 0)), (string) ((int) ($data['skipped_count'] ?? 0)), (string) ((int) ($data['failed_count'] ?? 0)), $data['error_codes_json'] ?? null, $data['duration_ms'] !== null ? (string) ((int) $data['duration_ms']) : null, (string) $id ); return (int) $updated > 0; } public function listPaged(array $filters): array { $search = trim((string) ($filters['search'] ?? '')); $profileKey = strtolower(trim((string) ($filters['profile_key'] ?? ''))); $status = strtolower(trim((string) ($filters['status'] ?? ''))); $createdFrom = trim((string) ($filters['created_from'] ?? '')); $createdTo = trim((string) ($filters['created_to'] ?? '')); $userIds = array_slice( RepoQuery::normalizeIdList($filters['user_ids'] ?? ''), 0, self::FILTER_OPTIONS_LIMIT_MAX ); [$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 200, 0); [$order, $dir] = RepoQuery::sanitizeOrder( $filters, [ 'id', 'started_at', 'finished_at', 'duration_ms', 'rows_total', 'created_count', 'skipped_count', 'failed_count', 'status', 'profile_key', ], 'started_at', 'desc' ); $where = []; $params = []; RepoQuery::addLikeFilter( $where, $params, [ 'import_audit_runs.run_uuid', 'import_audit_runs.source_filename', 'import_audit_runs.error_codes_json', ], $search ); if (in_array($profileKey, ['users', 'departments'], true)) { $where[] = 'import_audit_runs.profile_key = ?'; $params[] = $profileKey; } if (ImportAuditStatus::tryNormalize($status) !== null) { $where[] = 'import_audit_runs.status = ?'; $params[] = $status; } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdFrom)) { $where[] = 'import_audit_runs.started_at >= ?'; $params[] = $createdFrom . ' 00:00:00'; } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdTo)) { $where[] = 'import_audit_runs.started_at <= ?'; $params[] = $createdTo . ' 23:59:59'; } if ($userIds !== []) { $where[] = 'import_audit_runs.user_id in (???)'; $params[] = $userIds; } $whereSql = $where ? (' where ' . implode(' and ', $where)) : ''; $fromSql = ' from import_audit_runs ' . 'left join users on users.id = import_audit_runs.user_id ' . 'left join tenants on tenants.id = import_audit_runs.current_tenant_id '; $total = (int) (DB::selectValue('select count(*)' . $fromSql . $whereSql, ...$params) ?? 0); $rows = DB::select( 'select import_audit_runs.id, import_audit_runs.run_uuid, import_audit_runs.profile_key, import_audit_runs.status, import_audit_runs.source_filename, import_audit_runs.mapped_targets_csv, import_audit_runs.rows_total, import_audit_runs.created_count, import_audit_runs.skipped_count, import_audit_runs.failed_count, import_audit_runs.error_codes_json, import_audit_runs.started_at, import_audit_runs.finished_at, import_audit_runs.duration_ms, import_audit_runs.user_id, import_audit_runs.current_tenant_id, users.id, users.uuid, users.display_name, users.email, tenants.id, tenants.uuid, tenants.description ' . $fromSql . $whereSql . sprintf(' order by import_audit_runs.`%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 listFilterOptions(int $limit = self::FILTER_OPTIONS_LIMIT_MAX): array { $limit = max(1, min(self::FILTER_OPTIONS_LIMIT_MAX, $limit)); $userRows = DB::select( 'select import_audit_runs.user_id, max(import_audit_runs.started_at) as last_used_at, max(users.id) as user_exists_id, max(users.display_name) as user_display_name, max(users.email) as user_email from import_audit_runs left join users on users.id = import_audit_runs.user_id where import_audit_runs.user_id is not null and import_audit_runs.user_id > 0 group by import_audit_runs.user_id order by last_used_at desc limit ?', (string) $limit ); $users = []; if (is_array($userRows)) { foreach ($userRows as $row) { $flat = RepositoryArrayHelper::flattenRow($row); $id = (int) ($flat['user_id'] ?? 0); if ($id <= 0) { continue; } $users[] = [ 'id' => $id, 'display_name' => trim((string) ($flat['user_display_name'] ?? '')), 'email' => trim((string) ($flat['user_email'] ?? '')), 'exists' => (int) ($flat['user_exists_id'] ?? 0) > 0, ]; } } return ['users' => $users]; } public function find(int $id): ?array { if ($id <= 0) { return null; } $row = DB::selectOne( 'select import_audit_runs.id, import_audit_runs.run_uuid, import_audit_runs.profile_key, import_audit_runs.status, import_audit_runs.source_filename, import_audit_runs.mapped_targets_csv, import_audit_runs.rows_total, import_audit_runs.created_count, import_audit_runs.skipped_count, import_audit_runs.failed_count, import_audit_runs.error_codes_json, import_audit_runs.started_at, import_audit_runs.finished_at, import_audit_runs.duration_ms, import_audit_runs.user_id, import_audit_runs.current_tenant_id, users.id, users.uuid, users.display_name, users.email, tenants.id, tenants.uuid, tenants.description from import_audit_runs left join users on users.id = import_audit_runs.user_id left join tenants on tenants.id = import_audit_runs.current_tenant_id where import_audit_runs.id = ? limit 1', (string) $id ); return $this->normalizeRow($row); } public function purgeOlderThanDays(int $days): int { if ($days <= 0) { return 0; } $cutoff = (new \DateTimeImmutable('now', new \DateTimeZone('UTC'))) ->modify('-' . $days . ' days') ->format('Y-m-d H:i:s'); $deleted = DB::delete('delete from import_audit_runs where started_at < ?', $cutoff); return is_int($deleted) ? $deleted : 0; } private function normalizeRow(mixed $row): ?array { if (!is_array($row)) { return null; } $item = $row['import_audit_runs'] ?? []; if (!is_array($item) || !isset($item['id'])) { return null; } $user = is_array($row['users'] ?? null) ? $row['users'] : []; $tenant = is_array($row['tenants'] ?? null) ? $row['tenants'] : []; $item['user_uuid'] = (string) ($user['uuid'] ?? ''); $item['user_display_name'] = (string) ($user['display_name'] ?? ''); $item['user_email'] = (string) ($user['email'] ?? ''); $item['current_tenant_uuid'] = (string) ($tenant['uuid'] ?? ''); $item['current_tenant_description'] = (string) ($tenant['description'] ?? ''); return $item; } }