diff --git a/src/app/job-details/job-details.component.ts b/src/app/job-details/job-details.component.ts index 287469b..74967c2 100644 --- a/src/app/job-details/job-details.component.ts +++ b/src/app/job-details/job-details.component.ts @@ -69,7 +69,7 @@ export class JobDetailsComponent implements OnInit { public getRelatedJobs(): any { if (this.job && this.job.publishedCategory) { - this.service.getjobs({ 'publishedCategory.id': [this.job.publishedCategory.id]}, {} , SettingsService.settings.service.batchSize).subscribe((res: any) => { this.relatedJobs = res.data; }); + this.service.getJobs({ 'publishedCategory.id': [this.job.publishedCategory.id]}, {} , SettingsService.settings.service.batchSize).subscribe((res: any) => { this.relatedJobs = res.data; }); } } diff --git a/src/app/job-list/job-list.component.ts b/src/app/job-list/job-list.component.ts index d6ed066..408e466 100644 --- a/src/app/job-list/job-list.component.ts +++ b/src/app/job-list/job-list.component.ts @@ -40,7 +40,7 @@ export class JobListComponent implements OnChanges { this.meta.updateTag({ name: 'og:description', content: description }); this.meta.updateTag({ name: 'twitter:description', content: description }); this.meta.updateTag({ name: 'description', content: description }); - this.http.getjobs(this.filter, { start: this.start }).subscribe(this.onSuccess.bind(this), this.onFailure.bind(this)); + this.http.getJobs(this.filter, { start: this.start }).subscribe(this.onSuccess.bind(this), this.onFailure.bind(this)); } public loadMore(): void { diff --git a/src/app/services/search/search.service.ts b/src/app/services/search/search.service.ts index e699209..8262385 100644 --- a/src/app/services/search/search.service.ts +++ b/src/app/services/search/search.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { SettingsService } from '../settings/settings.service'; -import { Observable, of } from 'rxjs'; +import { Observable, of, forkJoin } from 'rxjs'; import { IServiceSettings } from '../../typings/settings'; +import { concatMap, map } from 'rxjs/operators'; @Injectable() export class SearchService { @@ -17,7 +18,7 @@ export class SearchService { return `${scheme}://public-rest${service?.swimlane}.bullhornstaffing.com:${port}/rest-services/${service?.corpToken}`; } - public getjobs(filter?: any, params: any = {}, count: number = 30): Observable { + public getJobs(filter?: any, params: any = {}, count: number = 30): Observable { let queryArray: string[] = []; params.query = `(isOpen:1) AND (isDeleted:0)${this.formatAdditionalCriteria(true)}${this.formatFilter(filter, true)}`; params.fields = SettingsService.settings.service.fields; @@ -37,55 +38,136 @@ export class SearchService { return this.http.get(`${this.baseUrl}/query/JobBoardPost?where=(id=${id})&fields=${SettingsService.settings?.service?.fields}`); } - public getCurrentJobIds(filter: any, ignoreFields: string[]): Observable { - let queryArray: string[] = []; - let params: any = {}; + public getCurrentJobIds(filter: any, ignoreFields: string[]): Observable { + const queryString: string = this.getQueryString(filter, ignoreFields); + + // Recursive function to fetch all records + const fetchAllRecords = (start: number = 0, records: any[] = []): Observable => { + return this.getJobRecords(queryString, start).pipe( + concatMap((response: any) => { + // Concatenate records from the response + const updatedRecords = [...records, ...response.data]; + + if (updatedRecords.length < response.total) { + // Continue fetching more records if needed + return fetchAllRecords(updatedRecords.length, updatedRecords); + } else { + // Return the accumulated records when done + return of(updatedRecords); + } + }), + ); + }; + + // Start fetching all records + return fetchAllRecords(); + } + + private getQueryString(filter: any, ignoreFields: string[]): string { + // Construct the query string based on filter and parameters + const params = { + query: `(isOpen:1) AND (isDeleted:0)${this.formatAdditionalCriteria(true)}${this.formatFilter(filter, true, ignoreFields)}`, + count: `500`, + fields: 'id', + sort: 'id' + }; + + // Join the query parameters with '&' to form the complete query string + return Object.entries(params).map(([key, value]) => `${key}=${value}`).join('&'); + } + + private getJobRecords(queryString: string, start: number = 0): Observable { + // Fetch job records from the API with the specified query and start offset + return this.http.get(`${this.baseUrl}/search/JobOrder?start=${start}&${queryString}`); + } - params.query = `(isOpen:1) AND (isDeleted:0)${this.formatAdditionalCriteria(true)}${this.formatFilter(filter, true, ignoreFields)}`; - params.count = `500`; - params.fields = 'id'; - params.sort = 'id'; +// Function to get available filter options +public getAvailableFilterOptions(ids: number[], field: string): Observable { + // If there are no ids, return an empty response + if(ids.length === 0) { + return of({count:0, start:0, data:[]}); + } - for (let key in params) { - queryArray.push(`${key}=${params[key]}`); + // Define the batch size + const batchSize = 500; + + // Create an array of observables for each batch of ids + const observables = Array(Math.ceil(ids.length / batchSize)).fill(null).map((_, index) => { + // Get the ids for the current batch + const batchIds = ids.slice(index * batchSize, (index + 1) * batchSize); + + // Define the parameters for the HTTP request + const params: any = { + count: 500, + fields: `${field},count(id)`, + groupBy: field, + where: `id IN (${batchIds.toString()})`, + orderBy: this.getOrderByField(field) // Get the order by field based on the field parameter } - let queryString: string = queryArray.join('&'); - return this.http.get(`${this.baseUrl}/search/JobOrder?${queryString}`); - } + // Create the query string from the parameters + const queryString = Object.keys(params).map(key => `${key}=${params[key]}`).join('&'); + + // Return the observable for the HTTP request + return this.http.get(`${this.baseUrl}/query/JobBoardPost?${queryString}`); + }); + + // Use forkJoin to wait for all observables to complete and then process the responses + return forkJoin(observables).pipe( + map((responses: any[]) => { + // Reduce the responses to a single response by merging the data + const mergedResponse = responses.reduce((acc, response) => { + // For each item in the response data + response.data.forEach(item => { + // Find the index of the existing item in the accumulator data + const existingItemIndex = acc.data.findIndex(x => this.isSameItem(x, item, field)); + + // If the item exists, increment its count + if(existingItemIndex !== -1) { + acc.data[existingItemIndex].idCount += item.idCount; + } else { + // If the item does not exist, add it to the accumulator data + acc.data.push(item); + } + }) + + // Return the accumulator + return acc; + }, {count: 0, start: 0, data: []}); + + // Return the merged response + return mergedResponse; + }), + ) +} - public getAvailableFilterOptions(ids: number[], field: string): Observable { - let params: any = {}; - let queryArray: string[] = []; - if (ids.length > 0) { - params.where = `id IN (${ids.toString()})`; - params.count = `500`; - params.fields = `${field},count(id)`; - params.groupBy = field; - switch (field) { - case 'publishedCategory(id,name)': - params.orderBy = 'publishedCategory.name'; - break; - case 'address(state)': - params.orderBy = 'address.state'; - break; - case 'address(city)': - params.orderBy = 'address.city'; - break; - default: - params.orderBy = '-count.id'; - break; - } - for (let key in params) { - queryArray.push(`${key}=${params[key]}`); - } - let queryString: string = queryArray.join('&'); +// Function to get the order by field based on the field parameter +private getOrderByField(field: string): string { + switch (field) { + case 'publishedCategory(id,name)': + return 'publishedCategory.name'; + case 'address(state)': + return 'address.state'; + case 'address(city)': + return 'address.city'; + default: + return '-count.id'; + } +} - return this.http.get(`${this.baseUrl}/query/JobBoardPost?${queryString}`); // tslint:disable-line - } else { - return of({count: 0, start: 0, data: []}); - } +// Function to check if two items are the same based on the field parameter +private isSameItem(item1: any, item2: any, field: string): boolean { + switch(field) { + case 'publishedCategory(id,name)': + return item1?.publishedCategory?.id === item2?.publishedCategory?.id; + case 'address(state)': + return item1?.address?.state === item2?.address?.state; + case 'address(city)': + return item1?.address?.city === item2?.address?.city; + default: + return false; } +} private formatAdditionalCriteria(isSearch: boolean): string { let field: string = SettingsService.settings.additionalJobCriteria.field; @@ -123,5 +205,4 @@ export class SearchService { return additionalFilter.replace(/{\?\^\^equals}/g, isSearch ? ':' : '=').replace(/{\?\^\^delimiter}/g, isSearch ? '"' : '\''); } - -} +} \ No newline at end of file diff --git a/src/app/sidebar/sidebar-filter/sidebar-filter.component.ts b/src/app/sidebar/sidebar-filter/sidebar-filter.component.ts index 787774d..c5bb29f 100644 --- a/src/app/sidebar/sidebar-filter/sidebar-filter.component.ts +++ b/src/app/sidebar/sidebar-filter/sidebar-filter.component.ts @@ -21,7 +21,7 @@ export class SidebarFilterComponent implements OnChanges { public options: any[]; public fieldName: string; - constructor(private service: SearchService, private formUtils: FormUtils) { } + constructor(private service: SearchService, private formUtils: FormUtils) {} public ngOnChanges(changes: SimpleChanges): void { switch (this.field) { @@ -45,28 +45,31 @@ export class SidebarFilterComponent implements OnChanges { } private handleJobIdsOnSuccess(res: any): void { - let resultIds: number[] = res.data.map((result: any) => { return result.id; }); + let resultIds: number[] = res.map((result: any) => { + return result.id; + }); this.service.getAvailableFilterOptions(resultIds, this.field).subscribe(this.setFieldOptionsOnSuccess.bind(this)); - } private setFieldOptionsOnSuccess(res: any): void { let interaction: Function; switch (this.field) { case 'address(city)': - this.options = res.data.map((result: IAddressListResponse) => { - return { - value: result.address.city, - label: `${result.address.city} (${result.idCount})`, - }; - }).filter((item: any) => { - return item.value; - }); + this.options = res.data + .map((result: IAddressListResponse) => { + return { + value: result.address.city, + label: `${result.address.city} (${result.idCount})`, + }; + }) + .filter((item: any) => { + return item.value; + }); interaction = (API: FieldInteractionApi) => { let values: string[] = []; this.lastSetValue = API.getActiveValue(); if (API.getActiveValue()) { - values = API.getActiveValue().map((value: string ) => { + values = API.getActiveValue().map((value: string) => { return `address.city{?^^equals}{?^^delimiter}${value}{?^^delimiter}`; }); } @@ -74,19 +77,21 @@ export class SidebarFilterComponent implements OnChanges { }; break; case 'address(state)': - this.options = res.data.map((result: IAddressListResponse) => { - return { - value: result.address.state, - label: `${result.address.state} (${result.idCount})`, - }; - }).filter((item: any) => { - return item.value; - }); + this.options = res.data + .map((result: IAddressListResponse) => { + return { + value: result.address.state, + label: `${result.address.state} (${result.idCount})`, + }; + }) + .filter((item: any) => { + return item.value; + }); interaction = (API: FieldInteractionApi) => { let values: string[] = []; this.lastSetValue = API.getActiveValue(); if (API.getActiveValue()) { - values = API.getActiveValue().map((value: string ) => { + values = API.getActiveValue().map((value: string) => { return `address.state{?^^equals}{?^^delimiter}${value}{?^^delimiter}`; }); } @@ -95,22 +100,22 @@ export class SidebarFilterComponent implements OnChanges { break; case 'publishedCategory(id,name)': this.options = res.data - .filter((unfilteredResult: ICategoryListResponse) => { - return !!unfilteredResult.publishedCategory; - }) - .map((result: ICategoryListResponse) => { - return { - value: result.publishedCategory.id, - label: `${result.publishedCategory.name} (${result.idCount})`, - }; - }); + .filter((unfilteredResult: ICategoryListResponse) => { + return !!unfilteredResult.publishedCategory; + }) + .map((result: ICategoryListResponse) => { + return { + value: result.publishedCategory.id, + label: `${result.publishedCategory.name} (${result.idCount})`, + }; + }); interaction = (API: FieldInteractionApi) => { let values: string[] = []; this.lastSetValue = API.getActiveValue(); if (API.getActiveValue()) { - values = API.getActiveValue().map((value: number) => { - return `publishedCategory.id{?^^equals}${value}`; - }); + values = API.getActiveValue().map((value: number) => { + return `publishedCategory.id{?^^equals}${value}`; + }); } this.checkboxFilter.emit(values); }; @@ -122,11 +127,10 @@ export class SidebarFilterComponent implements OnChanges { this.control = new CheckListControl({ key: 'checklist', options: this.options, - interactions: [{event: 'change', script: interaction.bind(this), invokeOnInit: false}], + interactions: [{ event: 'change', script: interaction.bind(this), invokeOnInit: false }], }); - this.formUtils.setInitialValues([this.control], {'checklist': this.lastSetValue}); + this.formUtils.setInitialValues([this.control], { checklist: this.lastSetValue }); this.form = this.formUtils.toFormGroup([this.control]); this.loading = false; } - } diff --git a/src/app/sidebar/sidebar.component.ts b/src/app/sidebar/sidebar.component.ts index 85f8d46..f9877bf 100644 --- a/src/app/sidebar/sidebar.component.ts +++ b/src/app/sidebar/sidebar.component.ts @@ -70,16 +70,19 @@ export class SidebarComponent { }, 250); } - public updateFilter( - field: string, - httpFormatedFilter: string | string[], - ): void { + public updateFilter(field: string, httpFormatedFilter: string | string[]): void { delete this.filter['keyword']; - this.filter[field] = httpFormatedFilter; - let filter: object = {}; - Object.assign(filter, this.filter); - this.filter = filter; // triggering angular change detection - this.newFilter.emit(this.filter); + // max length of calling function changed to 500 to not break search. If more than 500, show all results until the filter is refined by user + if (Array.isArray(httpFormatedFilter) && httpFormatedFilter.length === 500) { + this.filter = {}; + this.newFilter.emit(this.filter); + } else { + this.filter[field] = httpFormatedFilter; + let filter: object = {}; + Object.assign(filter, this.filter); + this.filter = filter; // triggering angular change detection + this.newFilter.emit(this.filter); + } } public hideSidebar(): void { @@ -101,7 +104,9 @@ export class SidebarComponent { } private handleJobIdsOnSuccess(res: any): void { - let resultIds: string[] = res.data.map((result: any) => { + // only show results if filter is less than 500. If more than 500, show all results until the filter is refined by user + res = res.slice(0, 500); + let resultIds: string[] = res.map((result: any) => { return `id{?^^equals}${result.id}`; }); if (resultIds.length === 0) {