diff --git a/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs b/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs index 2b97d9ae..737f78de 100644 --- a/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs +++ b/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs @@ -25,7 +25,8 @@ public sealed class CustomFormatScoreController : ControllerBase public async Task GetCustomFormatScores( [FromQuery] int page = 1, [FromQuery] int pageSize = 50, - [FromQuery] Guid? instanceId = null) + [FromQuery] Guid? instanceId = null, + [FromQuery] string? search = null) { if (page < 1) page = 1; if (pageSize < 1) pageSize = 50; @@ -40,6 +41,11 @@ public sealed class CustomFormatScoreController : ControllerBase query = query.Where(e => e.ArrInstanceId == instanceId.Value); } + if (!string.IsNullOrWhiteSpace(search)) + { + query = query.Where(e => e.Title.Contains(search)); + } + int totalCount = await query.CountAsync(); var items = await query @@ -151,6 +157,29 @@ public sealed class CustomFormatScoreController : ControllerBase }); } + [HttpGet("instances")] + public async Task GetInstances() + { + var instances = await _dataContext.CustomFormatScoreEntries + .AsNoTracking() + .Select(e => new { e.ArrInstanceId, e.ItemType }) + .Distinct() + .Join( + _dataContext.ArrInstances.AsNoTracking(), + e => e.ArrInstanceId, + a => a.Id, + (e, a) => new + { + Id = e.ArrInstanceId, + a.Name, + e.ItemType, + }) + .OrderBy(x => x.Name) + .ToListAsync(); + + return Ok(new { Instances = instances }); + } + /// /// Gets summary statistics for CF score tracking. /// diff --git a/code/frontend/src/app/core/api/cf-score.api.ts b/code/frontend/src/app/core/api/cf-score.api.ts index cfa37044..2c476162 100644 --- a/code/frontend/src/app/core/api/cf-score.api.ts +++ b/code/frontend/src/app/core/api/cf-score.api.ts @@ -63,6 +63,12 @@ export interface CfScoreHistoryResponse { entries: CfScoreHistoryEntry[]; } +export interface CfScoreInstance { + id: string; + name: string; + itemType: string; +} + @Injectable({ providedIn: 'root' }) export class CfScoreApi { private http = inject(HttpClient); @@ -77,12 +83,17 @@ export class CfScoreApi { }); } - getScores(page = 1, pageSize = 50, search?: string): Observable { + getScores(page = 1, pageSize = 50, search?: string, instanceId?: string): Observable { const params: Record = { page, pageSize }; if (search) params['search'] = search; + if (instanceId) params['instanceId'] = instanceId; return this.http.get('/api/seeker/cf-scores', { params }); } + getInstances(): Observable<{ instances: CfScoreInstance[] }> { + return this.http.get<{ instances: CfScoreInstance[] }>('/api/seeker/cf-scores/instances'); + } + getItemHistory(instanceId: string, itemId: number, episodeId = 0): Observable { return this.http.get( `/api/seeker/cf-scores/${instanceId}/${itemId}/history`, diff --git a/code/frontend/src/app/features/cf-scores/cf-scores.component.html b/code/frontend/src/app/features/cf-scores/cf-scores.component.html index f2236039..86c1362a 100644 --- a/code/frontend/src/app/features/cf-scores/cf-scores.component.html +++ b/code/frontend/src/app/features/cf-scores/cf-scores.component.html @@ -7,6 +7,12 @@
+ (''); + readonly instanceOptions = signal([]); readonly expandedId = signal(null); readonly historyEntries = signal([]); readonly historyLoading = signal(false); ngOnInit(): void { + this.loadInstances(); this.loadScores(); this.loadStats(); this.pollTimer = setInterval(() => { @@ -68,7 +73,7 @@ export class CfScoresComponent implements OnInit, OnDestroy { loadScores(): void { this.loading.set(true); - this.api.getScores(this.currentPage(), this.pageSize(), this.searchQuery() || undefined).subscribe({ + this.api.getScores(this.currentPage(), this.pageSize(), this.searchQuery() || undefined, this.selectedInstanceId() || undefined).subscribe({ next: (result) => { this.items.set(result.items); this.totalRecords.set(result.totalCount); @@ -81,6 +86,26 @@ export class CfScoresComponent implements OnInit, OnDestroy { }); } + private loadInstances(): void { + this.api.getInstances().subscribe({ + next: (result) => { + this.instanceOptions.set([ + { label: 'All Instances', value: '' }, + ...result.instances.map(i => ({ + label: `${i.name} (${i.itemType})`, + value: i.id, + })), + ]); + }, + }); + } + + onInstanceFilterChange(value: string): void { + this.selectedInstanceId.set(value); + this.currentPage.set(1); + this.loadScores(); + } + private loadStats(): void { this.api.getStats().subscribe({ next: (stats) => this.stats.set(stats),