diff --git a/src/app/applications/application-add-edit/application-add-edit.component.html b/src/app/applications/application-add-edit/application-add-edit.component.html index 8aa526a4..05dddc4b 100644 --- a/src/app/applications/application-add-edit/application-add-edit.component.html +++ b/src/app/applications/application-add-edit/application-add-edit.component.html @@ -1,4 +1,4 @@ -
+
@@ -31,7 +31,7 @@

Crown land File: {{application['clFile']}}  &r

-
+
diff --git a/src/app/search/search.component.html b/src/app/search/search.component.html index 305ead38..ddfff115 100644 --- a/src/app/search/search.component.html +++ b/src/app/search/search.component.html @@ -26,7 +26,10 @@

{{count}} results found for "{{keywords.length > 0 ? keywords.join(', ') : 'unknown'}}"

- + +
@@ -37,40 +40,40 @@

- - + + - + - +
CL File
- {{item.properties.CROWN_LANDS_FILE}} + {{application['clFile']}} - {{item.properties.DISPOSITION_TRANSACTION_SID}} + {{application.tantalisID}} - {{item.properties.TENURE_PURPOSE | titlecase}} / {{item.properties.TENURE_SUBPURPOSE | titlecase}} + {{application.purpose | titlecase}} / {{application.subpurpose | titlecase}} - {{item.prcStatus || 'Unknown'}} + {{application.appStatus || 'Unknown'}} - -
+
- - - @@ -78,22 +81,23 @@

- + - {{item.app['numComments']}} {{item.app['numComments'] === 1 ? 'comment' : 'comments'}} + {{application['numComments']}} {{application['numComments'] === 1 ? 'comment' : 'comments'}}  -  - - {{item.app.cpStatus}} + + {{application.cpStatus || 'Unknown'}} - -  -  {{item.app.currentPeriod.startDate | date:'longDate'}} to {{item.app.currentPeriod.endDate | date:'longDate'}} - -  ({{item.app.currentPeriod['daysRemaining'] + (item.app.currentPeriod['daysRemaining'] === 1 ? ' day ' : ' days ') + 'remaining'}}) + +  -  + {{application.currentPeriod.startDate | date:'longDate'}} to {{application.currentPeriod.endDate | date:'longDate'}} + +  ({{application.currentPeriod['daysRemaining'] + (application.currentPeriod['daysRemaining'] === 1 ? ' day ' : ' days ') + 'remaining'}}) diff --git a/src/app/search/search.component.scss b/src/app/search/search.component.scss index 5eaea8f1..258f1d1a 100644 --- a/src/app/search/search.component.scss +++ b/src/app/search/search.component.scss @@ -79,7 +79,7 @@ $sr-table-row-bg: #f7f8fa; td { padding-top: 1.25rem; padding-right: 1rem; - padding-bottom: 0; + padding-bottom: 1.25rem; padding-left: 1rem; border: none; background: $sr-table-row-bg; @@ -128,7 +128,7 @@ $sr-table-row-bg: #f7f8fa; .app-comment-details { td { - padding-top: 0.75rem; + padding-top: 0; padding-bottom: 1.25rem; a { diff --git a/src/app/search/search.component.ts b/src/app/search/search.component.ts index c509b4a2..98092816 100644 --- a/src/app/search/search.component.ts +++ b/src/app/search/search.component.ts @@ -5,9 +5,9 @@ import { Subject } from 'rxjs/Subject'; import 'rxjs/add/operator/takeUntil'; import * as _ from 'lodash'; -import { SearchTerms } from 'app/models/search'; -import { ApplicationService } from 'app/services/application.service'; import { SearchService } from 'app/services/search.service'; +import { SearchTerms } from 'app/models/search'; +import { Application } from 'app/models/application'; @Component({ selector: 'app-search', @@ -20,15 +20,14 @@ export class SearchComponent implements OnInit, OnDestroy { public searching = false; public ranSearch = false; public keywords: Array = []; - public groupByResults: Array = []; + public applications: Array = []; public count = 0; // for template private snackBarRef: MatSnackBarRef = null; private ngUnsubscribe = new Subject(); constructor( public snackBar: MatSnackBar, - private applicationService: ApplicationService, - private searchService: SearchService, + public searchService: SearchService, // also used in template private router: Router, private route: ActivatedRoute ) { } @@ -59,48 +58,21 @@ export class SearchComponent implements OnInit, OnDestroy { private doSearch() { this.searching = true; + this.count = 0; this.keywords = this.terms.keywords && _.uniq(_.compact(this.terms.keywords.split(' '))) || []; // safety checks - this.groupByResults.length = 0; // empty the list + this.applications.length = 0; // empty the list - this.searchService.getByClidDtid(this.keywords) + this.searchService.getAppsByClidDtid(this.keywords) .takeUntil(this.ngUnsubscribe) .subscribe( - search => { - // console.log('search =', search); - if (search && search.totalFeatures > 0 && search.features && search.features.length > 0) { - const groupedFeatures = _.groupBy(search.features, 'properties.DISPOSITION_TRANSACTION_SID'); - const self = this; // for closure below - _.each(groupedFeatures, function (value: any, key: string) { - // display PRC status from properties - value[0].prcStatus = self.applicationService.getStatusString(self.applicationService.getStatusCode(value[0].properties.TENURE_STATUS)); - // ensure result is not already in list - if (!_.find(self.groupByResults, result => { return result.properties.DISPOSITION_TRANSACTION_SID === +key; })) { - // if app is in PRC, query application data to update UI - if (_.includes(search.sidsFound, key)) { - value[0].loaded = false; - self.applicationService.getByTantalisID(+key, { getCurrentPeriod: true }) - .takeUntil(self.ngUnsubscribe) - .subscribe( - application => { - value[0].loaded = true; - if (application) { - // display PRC status from application - value[0].prcStatus = application.appStatus; - value[0].app = application; - } - }, - error => { - console.log('error =', error); - self.snackBarRef = self.snackBar.open('Error retrieving application ...', null, { duration: 3000 }); - } - ); - } else { - value[0].loaded = true; - } - self.groupByResults.push(value[0]); - } - }); - } + applications => { + applications.forEach(application => { + // add if not already in list + if (!_.find(this.applications, app => { return app.tantalisID === application.tantalisID; })) { + this.applications.push(application); + } + }); + this.count = this.applications.length; }, error => { console.log('error =', error); @@ -108,7 +80,6 @@ export class SearchComponent implements OnInit, OnDestroy { // update variables on error this.searching = false; this.ranSearch = true; - this.count = 0; this.snackBarRef = this.snackBar.open('Error searching applications ...', 'RETRY'); this.snackBarRef.onAction().subscribe(() => this.onSubmit()); @@ -117,7 +88,6 @@ export class SearchComponent implements OnInit, OnDestroy { // update variables on completion this.searching = false; this.ranSearch = true; - this.count = this.groupByResults.length; }); } @@ -127,36 +97,36 @@ export class SearchComponent implements OnInit, OnDestroy { if (this.snackBarRef) { this.snackBarRef.dismiss(); } // NOTE: Angular Router doesn't reload page on same URL - // ref: https://stackoverflow.com/questions/40983055/how-to-reload-the-current-route-with-the-angular-2-router + // REF: https://stackoverflow.com/questions/40983055/how-to-reload-the-current-route-with-the-angular-2-router // WORKAROUND: add timestamp to force URL to be different than last time const params = this.terms.getParams(); - params['now'] = Date.now(); + params['ms'] = new Date().getMilliseconds(); // console.log('params =', params); this.router.navigate(['search', params]); } - public importProject(item: any) { - if (item.properties) { - // save application properties from search results + public onImport(application: Application) { + if (application) { + // save application data from search results const params = { - // initial cached data - purpose: item.properties.TENURE_PURPOSE, - subpurpose: item.properties.TENURE_SUBPURPOSE, - type: item.properties.TENURE_TYPE, - subtype: item.properties.TENURE_SUBTYPE, - status: item.properties.TENURE_STATUS, - tenureStage: item.properties.TENURE_STAGE, - location: item.properties.TENURE_LOCATION, - businessUnit: item.properties.RESPONSIBLE_BUSINESS_UNIT, - cl_file: item.properties.CROWN_LANDS_FILE, - tantalisID: item.properties.DISPOSITION_TRANSACTION_SID, - legalDescription: item.properties.TENURE_LEGAL_DESCRIPTION + // initial data + purpose: application.purpose, + subpurpose: application.subpurpose, + type: application.type, + subtype: application.subtype, + status: application.status, + tenureStage: application.tenureStage, + location: application.location, + businessUnit: application.businessUnit, + cl_file: application.cl_file, + tantalisID: application.tantalisID, + legalDescription: application.legalDescription }; // go to add-edit page this.router.navigate(['/a', 0, 'edit'], { queryParams: params }); } else { - console.log('error, invalid item =', item); + console.log('error, invalid application =', application); this.snackBarRef = this.snackBar.open('Error creating application ...', null, { duration: 3000 }); } } diff --git a/src/app/services/api.ts b/src/app/services/api.ts index 8c9cc13b..42462cd2 100644 --- a/src/app/services/api.ts +++ b/src/app/services/api.ts @@ -113,7 +113,7 @@ export class ApiService { return this.http.post(`${this.pathAPI}/login/token`, { username: username, password: password }) .map(res => { // login successful if there's a jwt token in the response - if (res.accessToken) { + if (res && res.accessToken) { this.token = res.accessToken; // store username and jwt token in local storage to keep user logged in between page refreshes @@ -199,6 +199,33 @@ export class ApiService { ); } + // NB: returns array + getApplicationsByCrownLandID(clid: string): Observable { + const fields = [ + 'agency', + 'areaHectares', + 'businessUnit', + 'centroid', + 'cl_file', + 'client', + 'description', + 'internal', + 'legalDescription', + 'location', + 'name', + 'publishDate', + 'purpose', + 'status', + 'subpurpose', + 'subtype', + 'tantalisID', + 'tenureStage', + 'type' + ]; + const queryString = `application?isDeleted=false&cl_file=${clid}&fields=${this.buildValues(fields)}`; + return this.http.get(`${this.pathAPI}/${queryString}`, {}); + } + // NB: returns array with 1 element getApplicationByTantalisId(tantalisId: number): Observable { const fields = [ @@ -598,17 +625,17 @@ export class ApiService { // // Searching // - getAppsByCLID(clid: string): Observable { + searchAppsByCLID(clid: string): Observable { const queryString = `public/search/bcgw/crownLandsId/${clid}`; return this.http.get(`${this.pathAPI}/${queryString}`, {}); } - getAppsByDTID(dtid: number): Observable { + searchAppsByDTID(dtid: number): Observable { const queryString = `public/search/bcgw/dispositionTransactionId/${dtid}`; return this.http.get(`${this.pathAPI}/${queryString}`, {}); } - getClientsByDTID(dtid: number): Observable { + searchClientsByDTID(dtid: number): Observable { const queryString = `public/search/bcgw/getClientsInfoByDispositionId/${dtid}`; return this.http.get(`${this.pathAPI}/${queryString}`, {}); } diff --git a/src/app/services/application.service.ts b/src/app/services/application.service.ts index e81b608b..75d38207 100644 --- a/src/app/services/application.service.ts +++ b/src/app/services/application.service.ts @@ -99,9 +99,36 @@ export class ApplicationService { return this.api.getApplications() .pipe( flatMap(apps => { + if (!apps || apps.length === 0) { + // NB: forkJoin([]) will complete immediately + // so return empty observable instead + return of([] as Application[]); + } + const observables: Array> = []; + apps.forEach(app => { + // now get the rest of the data for each application + observables.push(this._getExtraAppData(new Application(app), params || {})); + }); + return forkJoin(observables); + }) + ) + .catch(error => this.api.handleError(error)); + } + + // get applications by their Crown Land ID + getByCrownLandID(clid: string, params: GetParameters = null): Observable { + // first get just the applications + return this.api.getApplicationsByCrownLandID(clid) + .pipe( + flatMap(apps => { + if (!apps || apps.length === 0) { + // NB: forkJoin([]) will complete immediately + // so return empty observable instead + return of([] as Application[]); + } const observables: Array> = []; apps.forEach(app => { - // now get the rest of the data for this application + // now get the rest of the data for each application observables.push(this._getExtraAppData(new Application(app), params || {})); }); return forkJoin(observables); @@ -112,10 +139,13 @@ export class ApplicationService { // get a specific application by its Tantalis ID getByTantalisID(tantalisID: number, params: GetParameters = null): Observable { - // first get the base application data + // first get just the application return this.api.getApplicationByTantalisId(tantalisID) .pipe( flatMap(apps => { + if (!apps || apps.length === 0) { + return of(null as Application); + } // now get the rest of the data for this application return this._getExtraAppData(new Application(apps[0]), params || {}); }) @@ -125,10 +155,13 @@ export class ApplicationService { // get a specific application by its object id getById(appId: string, params: GetParameters = null): Observable { - // first get the base application data + // first get just the application return this.api.getApplication(appId) .pipe( flatMap(apps => { + if (!apps || apps.length === 0) { + return of(null as Application); + } // now get the rest of the data for this application return this._getExtraAppData(new Application(apps[0]), params || {}); }) @@ -233,7 +266,8 @@ export class ApplicationService { app.legalDescription = app.legalDescription.replace(/\n/g, '\\n'); } - return this.api.addApplication(app); + return this.api.addApplication(app) + .catch(error => this.api.handleError(error)); } // update existing application @@ -255,19 +289,23 @@ export class ApplicationService { app.legalDescription = app.legalDescription.replace(/\n/g, '\\n'); } - return this.api.saveApplication(app); + return this.api.saveApplication(app) + .catch(error => this.api.handleError(error)); } delete(app: Application): Observable { - return this.api.deleteApplication(app); + return this.api.deleteApplication(app) + .catch(error => this.api.handleError(error)); } publish(app: Application): Observable { - return this.api.publishApplication(app); + return this.api.publishApplication(app) + .catch(error => this.api.handleError(error)); } unPublish(app: Application): Observable { - return this.api.unPublishApplication(app); + return this.api.unPublishApplication(app) + .catch(error => this.api.handleError(error)); } /** diff --git a/src/app/services/search.service.ts b/src/app/services/search.service.ts index 179fc4c1..b7452727 100644 --- a/src/app/services/search.service.ts +++ b/src/app/services/search.service.ts @@ -1,22 +1,31 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { merge } from 'rxjs'; +import { of, merge, forkJoin } from 'rxjs'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/catch'; import * as _ from 'lodash'; import { ApiService } from './api'; +import { ApplicationService } from 'app/services/application.service'; import { SearchResults } from 'app/models/search'; import { Client } from 'app/models/client'; +import { Application } from 'app/models/application'; +import { Feature } from 'app/models/feature'; @Injectable() export class SearchService { - constructor(private api: ApiService) { } + public isBcgwError = false; + + constructor( + private api: ApiService, + private applicationService: ApplicationService + ) { } // get clients by Disposition Transaction ID getClientsByDTID(dtid: number): Observable { - return this.api.getClientsByDTID(dtid) + this.isBcgwError = false; + return this.api.searchClientsByDTID(dtid) .map(res => { if (res && res.length > 0) { const clients: Array = []; @@ -27,32 +36,140 @@ export class SearchService { } return []; }) - .catch(error => this.api.handleError(error)); + .catch(error => { + this.isBcgwError = true; + return this.api.handleError(error); + }); } // get search results by array of CLIDs or DTIDs - getByClidDtid(keys: string[]): Observable { - const observables = keys.map(key => { return this.getByCLID(key); }) - .concat(keys.map(key => { return this.getByDTID(+key); })); + getAppsByClidDtid(keys: string[]): Observable> { + this.isBcgwError = false; + const observables = keys.map(clid => { return this.getAppsByCLID(clid); }) + .concat(keys.map(dtid => { return this.getAppByDTID(+dtid); })); return merge(...observables) .catch(error => this.api.handleError(error)); } // get search results by CL File # - getByCLID(clid: string): Observable { - return this.api.getAppsByCLID(clid) + private getAppsByCLID(clid: string): Observable> { + const getByCrownLandID = this.applicationService.getByCrownLandID(clid, { getCurrentPeriod: true }); + + const searchAppsByCLID = this.api.searchAppsByCLID(clid) .map(res => { return res ? new SearchResults(res) : null; }) + .catch(() => { + this.isBcgwError = true; + // if search call fails, return null results + return of(null as SearchResults); + }); + + return forkJoin(getByCrownLandID, searchAppsByCLID) + .map(payloads => { + const applications: Array = payloads[0]; + const searchResults: SearchResults = payloads[1]; + const results: Array = []; + + // first look at PRC results + applications.forEach(app => { + app['isCreated'] = true; + results.push(app); + }); + + // now look at BCGW results + if (searchResults && searchResults.totalFeatures > 0 && searchResults.features && searchResults.features.length > 0) { + const groupedFeatures = _.groupBy(searchResults.features, 'properties.DISPOSITION_TRANSACTION_SID'); + _.each(groupedFeatures, (value: any, key: string) => { + const feature = new Feature(value[0]); + // add BCGW result if not already found in PRC + if (!_.find(results, app => { return app.tantalisID === feature.properties.DISPOSITION_TRANSACTION_SID; })) { + const app = new Application({ + purpose: feature.properties.TENURE_PURPOSE, + subpurpose: feature.properties.TENURE_SUBPURPOSE, + type: feature.properties.TENURE_TYPE, + subtype: feature.properties.TENURE_SUBTYPE, + status: feature.properties.TENURE_STATUS, + tenureStage: feature.properties.TENURE_STAGE, + location: feature.properties.TENURE_LOCATION, + businessUnit: feature.properties.RESPONSIBLE_BUSINESS_UNIT, + cl_file: feature.properties.CROWN_LANDS_FILE, + tantalisID: feature.properties.DISPOSITION_TRANSACTION_SID, + legalDescription: feature.properties.TENURE_LEGAL_DESCRIPTION + }); + // 7-digit CL File number for display + app['clFile'] = feature.properties.CROWN_LANDS_FILE.padStart(7, '0'); + // user-friendly application status + app.appStatus = this.applicationService.getStatusString(this.applicationService.getStatusCode(feature.properties.TENURE_STATUS)); + // derive region code + app.region = this.applicationService.getRegionCode(app.businessUnit); + results.push(app); + } + }); + } + + return results; + }) .catch(error => this.api.handleError(error)); } // get search results by Disposition Transaction ID - getByDTID(dtid: number): Observable { - return this.api.getAppsByDTID(dtid) + private getAppByDTID(dtid: number): Observable> { + const getByTantalisID = this.applicationService.getByTantalisID(dtid, { getCurrentPeriod: true }); + + const searchAppsByDTID = this.api.searchAppsByDTID(dtid) .map(res => { return res ? new SearchResults(res) : null; }) + .catch(() => { + this.isBcgwError = true; + // if call fails, return null results + return of(null as SearchResults); + }); + + return forkJoin(getByTantalisID, searchAppsByDTID) + .map(payloads => { + const application: Application = payloads[0]; + const searchResults: SearchResults = payloads[1]; + + // first look at PRC result + if (application) { + application['isCreated'] = true; + // found a unique application in PRC -- no need to look at BCGW results + return [application]; + } + + // now look at BCGW results + const results: Array = []; + if (searchResults && searchResults.totalFeatures > 0 && searchResults.features && searchResults.features.length > 0) { + const groupedFeatures = _.groupBy(searchResults.features, 'properties.DISPOSITION_TRANSACTION_SID'); + _.each(groupedFeatures, (value: any, key: string) => { + const feature = new Feature(value[0]); + const app = new Application({ + purpose: feature.properties.TENURE_PURPOSE, + subpurpose: feature.properties.TENURE_SUBPURPOSE, + type: feature.properties.TENURE_TYPE, + subtype: feature.properties.TENURE_SUBTYPE, + status: feature.properties.TENURE_STATUS, + tenureStage: feature.properties.TENURE_STAGE, + location: feature.properties.TENURE_LOCATION, + businessUnit: feature.properties.RESPONSIBLE_BUSINESS_UNIT, + cl_file: feature.properties.CROWN_LANDS_FILE, + tantalisID: feature.properties.DISPOSITION_TRANSACTION_SID, + legalDescription: feature.properties.TENURE_LEGAL_DESCRIPTION + }); + // 7-digit CL File number for display + app['clFile'] = feature.properties.CROWN_LANDS_FILE.padStart(7, '0'); + // user-friendly application status + app.appStatus = this.applicationService.getStatusString(this.applicationService.getStatusCode(feature.properties.TENURE_STATUS)); + // derive region code + app.region = this.applicationService.getRegionCode(app.businessUnit); + results.push(app); + }); + } + + return results; + }) .catch(error => this.api.handleError(error)); }