counties, pagination, filter count, show total results

This commit is contained in:
Andreas Knuth 2024-07-19 18:06:56 +02:00
parent abcde3991d
commit 9db23c2177
25 changed files with 67207 additions and 509 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,23 @@
import { Controller, Get, Param } from '@nestjs/common';
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { CountyRequest } from 'src/models/server.model.js';
import { GeoService } from './geo.service.js';
@Controller('geo')
export class GeoController {
constructor(private geoService:GeoService){}
constructor(private geoService: GeoService) {}
@Get(':prefix')
findByPrefix(@Param('prefix') prefix:string): any {
return this.geoService.findCitiesStartingWith(prefix);
}
@Get(':prefix')
findByPrefix(@Param('prefix') prefix: string): any {
return this.geoService.findCitiesStartingWith(prefix);
}
@Get(':prefix/:state')
findByPrefixAndState(@Param('prefix') prefix:string,@Param('state') state:string): any {
return this.geoService.findCitiesStartingWith(prefix,state);
}
@Get(':prefix/:state')
findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any {
return this.geoService.findCitiesStartingWith(prefix, state);
}
@Post('counties')
findByPrefixAndStates(@Body() countyRequest: CountyRequest): any {
return this.geoService.findCountiesStartingWith(countyRequest.prefix, countyRequest.states);
}
}

View File

@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { readFileSync } from 'fs';
import path, { join } from 'path';
import { GeoResult } from 'src/models/main.model.js';
import { CountyResult, GeoResult } from 'src/models/main.model.js';
import { fileURLToPath } from 'url';
import { City, Geo, State } from '../models/server.model.js';
import { City, CountyData, Geo, State } from '../models/server.model.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -11,6 +11,7 @@ const __dirname = path.dirname(__filename);
@Injectable()
export class GeoService {
geo: Geo;
counties: CountyData[];
constructor() {
this.loadGeo();
}
@ -18,9 +19,32 @@ export class GeoService {
const filePath = join(__dirname, '../..', 'assets', 'geo.json');
const rawData = readFileSync(filePath, 'utf8');
this.geo = JSON.parse(rawData);
const countiesFilePath = join(__dirname, '../..', 'assets', 'counties.json');
const rawCountiesData = readFileSync(countiesFilePath, 'utf8');
this.counties = JSON.parse(rawCountiesData);
}
findCountiesStartingWith(prefix: string, states?: string[]) {
let results: CountyResult[] = [];
let idCounter = 1;
findCitiesStartingWith(prefix: string, state?: string): { city: string; state: string; state_code: string }[] {
this.counties.forEach(stateData => {
if (!states || states.includes(stateData.state)) {
stateData.counties.forEach(county => {
if (county.startsWith(prefix.toUpperCase())) {
results.push({
id: idCounter++,
name: county,
state: stateData.state_full,
state_code: stateData.state,
});
}
});
}
});
return results;
}
findCitiesStartingWith(prefix: string, state?: string): GeoResult[] {
const result: GeoResult[] = [];
this.geo.states.forEach((state: State) => {

View File

@ -29,7 +29,10 @@ export class BusinessListingsController {
find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
return this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
}
@Post('findTotal')
findTotal(@Body() criteria: BusinessListingCriteria): Promise<number> {
return this.listingsService.getBusinessListingsCount(criteria);
}
// @UseGuards(OptionalJwtAuthGuard)
// @Post('search')
// search(@Request() req, @Body() criteria: BusinessListingCriteria): any {

View File

@ -31,6 +31,10 @@ export class CommercialPropertyListingsController {
async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> {
return await this.listingsService.searchCommercialProperties(criteria, req.user as JwtUser);
}
@Post('findTotal')
findTotal(@Body() criteria: CommercialPropertyListingCriteria): Promise<number> {
return this.listingsService.getCommercialPropertiesCount(criteria);
}
@Get('states/all')
getStates(): any {
return this.listingsService.getStates();

View File

@ -57,7 +57,6 @@ export interface ListCriteria {
start: number;
length: number;
page: number;
pageCount: number;
types: string[];
city: string;
prompt: string;
@ -230,6 +229,12 @@ export interface GeoResult {
state: string;
state_code: string;
}
export interface CountyResult {
id: number;
name: string;
state: string;
state_code: string;
}
export function isEmpty(value: any): boolean {
// Check for undefined or null
if (value === undefined || value === null) {

View File

@ -61,3 +61,12 @@ export interface Timezone {
abbreviation: string;
tzName: string;
}
export interface CountyData {
state: string;
state_full: string;
counties: string[];
}
export interface CountyRequest {
prefix: string;
states: string[];
}

View File

@ -45,6 +45,10 @@ export class UserController {
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
return foundUsers;
}
@Post('findTotal')
findTotal(@Body() criteria: UserListingCriteria): Promise<number> {
return this.userService.getUserListingsCount(criteria);
}
@Get('states/all')
async getStates(): Promise<any[]> {
this.logger.info(`Getting all states for users`);

View File

@ -0,0 +1,68 @@
import * as fs from 'fs';
import * as readline from 'readline';
interface CityData {
city: string;
stateShort: string;
stateFull: string;
county: string;
cityAlias: string;
}
interface StateCountyData {
state: string;
state_full: string;
counties: string[];
}
async function parseData(filePath: string): Promise<CityData[]> {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
const data: CityData[] = [];
let isFirstLine = true;
for await (const line of rl) {
if (isFirstLine) {
isFirstLine = false;
continue; // Skip the first line
}
const [city, stateShort, stateFull, county, cityAlias] = line.split('|');
data.push({ city, stateShort, stateFull, county, cityAlias });
}
return data;
}
function transformData(data: CityData[]): StateCountyData[] {
const stateMap: { [key: string]: { stateFull: string; counties: Set<string> } } = {};
data.forEach(item => {
if (!stateMap[item.stateShort]) {
stateMap[item.stateShort] = {
stateFull: item.stateFull,
counties: new Set(),
};
}
stateMap[item.stateShort].counties.add(item.county);
});
return Object.entries(stateMap).map(([state, value]) => ({
state,
state_full: value.stateFull,
counties: Array.from(value.counties).sort(),
}));
}
async function main() {
const filePath = './src/assets/counties_raw.csv'; // Ersetze diesen Pfad mit dem Pfad zu deiner Datei
const cityData = await parseData(filePath);
const stateCountyData = transformData(cityData);
console.log(JSON.stringify(stateCountyData, null, 2));
}
main().catch(err => console.error(err));

View File

@ -125,7 +125,7 @@
id="filterDropdownButton"
class="max-sm:hidden px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 md:me-2"
>
<i class="fas fa-filter mr-2"></i>Filter (1)
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
</button>
}
<button
@ -242,7 +242,7 @@
id="filterDropdownMobileButton"
class="w-full mx-4 px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
<i class="fas fa-filter mr-2"></i>Filter (1)
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
</button>
</div>
}

View File

@ -11,10 +11,11 @@ import { filter, Observable, Subject, Subscription } from 'rxjs';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment';
import { CriteriaChangeService } from '../../services/criteria-change.service';
import { SearchService } from '../../services/search.service';
import { SharedService } from '../../services/shared.service';
import { UserService } from '../../services/user.service';
import { getCriteriaStateObject, getSessionStorageHandlerWrapper, map2User } from '../../utils/utils';
import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaStateObject, map2User } from '../../utils/utils';
import { DropdownComponent } from '../dropdown/dropdown.component';
import { ModalService } from '../search-modal/modal.service';
@Component({
@ -40,6 +41,7 @@ export class HeaderComponent {
private subscription: Subscription;
criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
private routerSubscription: Subscription | undefined;
baseRoute: string;
constructor(
public keycloakService: KeycloakService,
private router: Router,
@ -48,6 +50,7 @@ export class HeaderComponent {
private breakpointObserver: BreakpointObserver,
private modalService: ModalService,
private searchService: SearchService,
private criteriaChangeService: CriteriaChangeService,
) {}
async ngOnInit() {
@ -75,18 +78,46 @@ export class HeaderComponent {
});
}
private checkCurrentRoute(url: string): void {
const baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/'
this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/'
const specialRoutes = [, '', ''];
if ('businessListings' === baseRoute) {
this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandlerWrapper('business'));
} else if ('commercialPropertyListings' === baseRoute) {
this.criteria = onChange(getCriteriaStateObject('commercialProperty'), getSessionStorageHandlerWrapper('commercialProperty'));
} else if ('brokerListings' === baseRoute) {
this.criteria = onChange(getCriteriaStateObject('broker'), getSessionStorageHandlerWrapper('broker'));
if ('businessListings' === this.baseRoute) {
//this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandlerWrapper('business'));
//this.criteria = onChange(getCriteriaStateObject('business'), this.getSessionStorageHandler);
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('business'));
} else if ('commercialPropertyListings' === this.baseRoute) {
// this.criteria = onChange(getCriteriaStateObject('commercialProperty'), getSessionStorageHandlerWrapper('commercialProperty'));
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('commercialProperty'));
} else if ('brokerListings' === this.baseRoute) {
// this.criteria = onChange(getCriteriaStateObject('broker'), getSessionStorageHandlerWrapper('broker'));
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('broker'));
} else {
this.criteria = undefined;
}
}
private createEnhancedProxy(obj: any) {
const component = this;
const sessionStorageHandler = function (path, value, previous, applyData) {
let criteriaType = '';
if ('/businessListings' === window.location.pathname) {
criteriaType = 'business';
} else if ('/commercialPropertyListings' === window.location.pathname) {
criteriaType = 'commercialProperty';
} else if ('/brokerListings' === window.location.pathname) {
criteriaType = 'broker';
}
sessionStorage.setItem(`${criteriaType}_criteria`, JSON.stringify(this));
};
return onChange(obj, function (path, value, previous, applyData) {
// Call the original sessionStorageHandler
sessionStorageHandler.call(this, path, value, previous, applyData);
// Notify about the criteria change using the component's context
component.criteriaChangeService.notifyCriteriaChange();
});
}
ngAfterViewInit() {}
async openModal() {
@ -94,13 +125,6 @@ export class HeaderComponent {
if (accepted) {
this.searchService.search(this.criteria);
}
// if (this.isActive('/businessListings')) {
// this.modalService.showModal(createEmptyBusinessListingCriteria());
// } else if (this.isActive('/commercialPropertyListings')) {
// this.modalService.showModal(createEmptyCommercialPropertyListingCriteria());
// } else if (this.isActive('/brokerListings')) {
// this.modalService.showModal(createEmptyUserListingCriteria());
// }
}
navigateWithState(dest: string, state: any) {
this.router.navigate([dest], { state: state });
@ -146,4 +170,15 @@ export class HeaderComponent {
this.destroy$.next();
this.destroy$.complete();
}
getNumberOfFiltersSet() {
if (this.criteria?.criteriaType === 'broker') {
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page']);
} else if (this.criteria?.criteriaType === 'business') {
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page']);
} else if (this.criteria?.criteriaType === 'commercialProperty') {
return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page']);
} else {
return 0;
}
}
}

View File

@ -1,4 +1,3 @@
<!-- <div class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full"> -->
<div *ngIf="modalService.modalVisible$ | async" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
<div class="relative w-full max-w-4xl max-h-full">
<div class="relative bg-white rounded-lg shadow">
@ -21,20 +20,12 @@
<div class="space-y-4">
<div>
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
<!-- <select id="state" [(ngModel)]="criteria.state" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option selected>Arkansas</option>
</select> -->
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [(ngModel)]="criteria.state" name="state"> </ng-select>
</div>
<div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<!-- <input
type="text"
id="city"
[(ngModel)]="criteria.city"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Houston"
/> -->
<ng-select
class="custom"
[multiple]="false"
@ -220,13 +211,6 @@
</div>
<div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<!-- <input
type="text"
id="city"
[(ngModel)]="criteria.city"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Houston"
/> -->
<ng-select
class="custom"
[multiple]="false"
@ -304,19 +288,26 @@
</div>
<div>
<label for="counties" class="block mb-2 text-sm font-medium text-gray-900">Locations served - Counties</label>
<select id="counties" [(ngModel)]="criteria.counties" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option selected>Arkansas</option>
</select>
<ng-select
[items]="counties$ | async"
bindLabel="name"
class="custom"
[multiple]="true"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="countyLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="countyInput$"
[(ngModel)]="criteria.counties"
>
<!-- @for (county of counties$ | async; track county.id) {
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
} -->
</ng-select>
</div>
<div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<!-- <input
type="text"
id="city"
[(ngModel)]="criteria.city"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Houston"
/> -->
<ng-select
class="custom"
[multiple]="false"
@ -359,7 +350,7 @@
</div>
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b">
<button type="button" (click)="modalService.accept()" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
Search
Search ({{ numberOfResults$ | async }})
</button>
<button
type="button"

View File

@ -1,10 +1,13 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { NgSelectModule } from '@ng-select/ng-select';
import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, GeoResult, KeyValue, KeyValueStyle, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { catchError, concat, debounceTime, distinctUntilChanged, map, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, CountyResult, GeoResult, KeyValue, KeyValueStyle, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { CriteriaChangeService } from '../../services/criteria-change.service';
import { GeoService } from '../../services/geo.service';
import { ListingsService } from '../../services/listings.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { SharedModule } from '../../shared/shared/shared.module';
import { ModalService } from './modal.service';
@ -17,17 +20,33 @@ import { ModalService } from './modal.service';
})
export class SearchModalComponent {
cities$: Observable<GeoResult[]>;
counties$: Observable<CountyResult[]>;
cityLoading = false;
countyLoading = false;
cityInput$ = new Subject<string>();
constructor(public selectOptions: SelectOptionsService, public modalService: ModalService, private geoService: GeoService) {}
countyInput$ = new Subject<string>();
private criteriaChangeSubscription: Subscription;
public criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
numberOfResults$: Observable<number>;
constructor(
public selectOptions: SelectOptionsService,
public modalService: ModalService,
private geoService: GeoService,
private criteriaChangeService: CriteriaChangeService,
private listingService: ListingsService,
private userService: UserService,
) {}
ngOnInit() {
this.setupCriteriaChangeListener();
this.modalService.message$.subscribe(msg => {
this.criteria = msg;
this.setTotalNumberOfResults();
});
this.loadCities();
this.loadCounties();
}
public criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
ngOnChanges() {}
categoryClicked(checked: boolean, value: string) {
if (checked) {
this.criteria.types.push(value);
@ -54,13 +73,34 @@ export class SearchModalComponent {
),
);
}
private loadCounties() {
this.counties$ = concat(
of([]), // default items
this.countyInput$.pipe(
distinctUntilChanged(),
tap(() => (this.countyLoading = true)),
switchMap(term =>
this.geoService.findCountiesStartingWith(term).pipe(
catchError(() => of([])), // empty list on error
map(counties => counties.map(county => county.name)), // transform the list of objects to a list of city names
tap(() => (this.countyLoading = false)),
),
),
),
);
}
private setupCriteriaChangeListener() {
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => this.setTotalNumberOfResults());
}
trackByFn(item: GeoResult) {
return item.id;
}
search() {
console.log('Search criteria:', this.criteria);
}
getCounties() {
this.geoService.findCountiesStartingWith('');
}
closeModal() {
console.log('Closing modal');
}
@ -70,4 +110,16 @@ export class SearchModalComponent {
isTypeOfProfessionalClicked(v: KeyValue) {
return this.criteria.types.find(t => t === v.value);
}
setTotalNumberOfResults() {
if (this.criteria) {
console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
if (this.criteria.criteriaType === 'business' || this.criteria.criteriaType === 'commercialProperty') {
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria, this.criteria.criteriaType);
} else if (this.criteria.criteriaType === 'broker') {
this.numberOfResults$ = this.userService.getNumberOfBroker(this.criteria);
} else {
this.numberOfResults$ = of();
}
}
}
}

View File

@ -95,3 +95,6 @@
}
</div>
</div>
@if(pageCount>1){
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
}

View File

@ -3,8 +3,9 @@ import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingType, UserListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { LISTINGS_PER_PAGE, ListingType, UserListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SearchService } from '../../../services/search.service';
@ -15,7 +16,7 @@ import { getCriteriaStateObject } from '../../../utils/utils';
@Component({
selector: 'app-broker-listings',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, NgOptimizedImage],
imports: [CommonModule, FormsModule, RouterModule, NgOptimizedImage, PaginatorComponent],
templateUrl: './broker-listings.component.html',
styleUrls: ['./broker-listings.component.scss', '../../pages.scss'],
})
@ -39,6 +40,8 @@ export class BrokerListingsComponent {
env = environment;
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
emailToDirName = emailToDirName;
page = 1;
pageCount = 1;
constructor(
public selectOptions: SelectOptionsService,
private listingsService: ListingsService,
@ -51,14 +54,6 @@ export class BrokerListingsComponent {
private searchService: SearchService,
) {
this.criteria = getCriteriaStateObject('broker');
// this.route.data.subscribe(async () => {
// if (this.router.getCurrentNavigation().extras.state) {
// } else {
// this.first = this.criteria.page * this.criteria.length;
// this.rows = this.criteria.length;
// }
// this.init();
// });
this.init();
this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'broker') {
@ -74,24 +69,20 @@ export class BrokerListingsComponent {
async init() {
this.search();
}
refine() {
this.criteria.start = 0;
this.criteria.page = 0;
this.search();
}
async search() {
const usersReponse = await this.userService.search(this.criteria);
this.users = usersReponse.results;
this.totalRecords = usersReponse.totalCount;
this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
onPageChange(event: any) {
this.criteria.start = event.first;
this.criteria.length = event.rows;
this.criteria.page = event.page;
this.criteria.pageCount = event.pageCount;
onPageChange(page: any) {
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE;
this.criteria.length = LISTINGS_PER_PAGE;
this.criteria.page = page;
this.search();
}
reset() {}
}

View File

@ -112,7 +112,9 @@
}
</div>
</div>
@if(pageCount>1){
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
}
<!-- <div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">

View File

@ -50,14 +50,6 @@ export class BusinessListingsComponent {
private searchService: SearchService,
) {
this.criteria = getCriteriaStateObject('business');
// this.route.data.subscribe(async () => {
// if (this.router.getCurrentNavigation().extras.state) {
// } else {
// this.first = this.criteria.page * this.criteria.length;
// this.rows = this.criteria.length;
// }
// this.init();
// });
this.init();
this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'business') {
@ -66,20 +58,14 @@ export class BusinessListingsComponent {
}
});
}
async ngOnInit() {
//initFlowbite();
}
async ngOnInit() {}
async init() {
this.reset();
const statesResult = await this.listingsService.getAllStates('business');
this.states = statesResult.map(ls => ({ name: this.selectOptions.getState(ls.state as string), value: ls.state, count: ls.count }));
this.search();
}
// refine() {
// this.criteria.start = 0;
// this.criteria.page = 0;
// this.search();
// }
async search() {
//this.listings = await this.listingsService.getListingsByPrompt(this.criteria);
const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
@ -90,10 +76,9 @@ export class BusinessListingsComponent {
this.cdRef.detectChanges();
}
onPageChange(page: any) {
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE + 1;
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE;
this.criteria.length = LISTINGS_PER_PAGE;
this.criteria.page = page;
// this.criteria.pageCount = event.pageCount;
this.search();
}
imageErrorHandler(listing: ListingType) {

View File

@ -78,3 +78,6 @@
}
</div>
</div>
@if(pageCount>1){
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
}

View File

@ -3,8 +3,9 @@ import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { CommercialPropertyListing } from '../../../../../../bizmatch-server/src/models/db.model';
import { CommercialPropertyListingCriteria, ResponseCommercialPropertyListingArray } from '../../../../../../bizmatch-server/src/models/main.model';
import { CommercialPropertyListingCriteria, LISTINGS_PER_PAGE, ResponseCommercialPropertyListingArray } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SearchService } from '../../../services/search.service';
@ -14,7 +15,7 @@ import { getCriteriaStateObject } from '../../../utils/utils';
@Component({
selector: 'app-commercial-property-listings',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule],
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent],
templateUrl: './commercial-property-listings.component.html',
styleUrls: ['./commercial-property-listings.component.scss', '../../pages.scss'],
})
@ -35,6 +36,8 @@ export class CommercialPropertyListingsComponent {
totalRecords: number = 0;
ts = new Date().getTime();
env = environment;
page = 1;
pageCount = 1;
constructor(
public selectOptions: SelectOptionsService,
private listingsService: ListingsService,
@ -46,14 +49,6 @@ export class CommercialPropertyListingsComponent {
private searchService: SearchService,
) {
this.criteria = getCriteriaStateObject('commercialProperty');
// this.route.data.subscribe(async () => {
// if (this.router.getCurrentNavigation().extras.state) {
// } else {
// this.first = this.criteria.page * this.criteria.length;
// this.rows = this.criteria.length;
// }
// this.init();
// });
this.init();
this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'commercialProperty') {
@ -77,14 +72,14 @@ export class CommercialPropertyListingsComponent {
const listingReponse = await this.listingsService.getListings(this.criteria, 'commercialProperty');
this.listings = (<ResponseCommercialPropertyListingArray>listingReponse).results;
this.totalRecords = (<ResponseCommercialPropertyListingArray>listingReponse).totalCount;
this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
onPageChange(event: any) {
this.criteria.start = event.first;
this.criteria.length = event.rows;
this.criteria.page = event.page;
this.criteria.pageCount = event.pageCount;
onPageChange(page: any) {
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE;
this.criteria.length = LISTINGS_PER_PAGE;
this.criteria.page = page;
this.search();
}
reset() {

View File

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class CriteriaChangeService {
private criteriaChangeSubject = new Subject<void>();
criteriaChange$ = this.criteriaChangeSubject.asObservable();
notifyCriteriaChange() {
this.criteriaChangeSubject.next();
}
}

View File

@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { GeoResult } from '../../../../bizmatch-server/src/models/main.model';
import { CountyResult, GeoResult } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({
@ -15,4 +15,7 @@ export class GeoService {
const stateString = state ? `/${state}` : '';
return this.http.get<GeoResult[]>(`${this.apiBaseUrl}/bizmatch/geo/${prefix}${stateString}`);
}
findCountiesStartingWith(prefix: string, states?: string[]): Observable<CountyResult[]> {
return this.http.post<CountyResult[]>(`${this.apiBaseUrl}/bizmatch/geo/counties`, { prefix, states });
}
}

View File

@ -26,6 +26,9 @@ export class ListingsService {
const result = await lastValueFrom(this.http.post<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/find`, criteria));
return result;
}
getNumberOfListings(criteria: BusinessListingCriteria | CommercialPropertyListingCriteria, listingsCategory: 'business' | 'commercialProperty'): Observable<number> {
return this.http.post<number>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/findTotal`, criteria);
}
async getListingsByPrompt(criteria: BusinessListingCriteria | CommercialPropertyListingCriteria): Promise<BusinessListing[]> {
const result = await lastValueFrom(this.http.post<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/business/search`, criteria));
return result;

View File

@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { lastValueFrom } from 'rxjs';
import { lastValueFrom, Observable } from 'rxjs';
import urlcat from 'urlcat';
import { User } from '../../../../bizmatch-server/src/models/db.model';
import { ResponseUsersArray, StatesResult, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
@ -30,6 +30,9 @@ export class UserService {
async search(criteria?: UserListingCriteria): Promise<ResponseUsersArray> {
return await lastValueFrom(this.http.post<ResponseUsersArray>(`${this.apiBaseUrl}/bizmatch/user/search`, criteria));
}
getNumberOfBroker(criteria?: UserListingCriteria): Observable<number> {
return this.http.post<number>(`${this.apiBaseUrl}/bizmatch/user/findTotal`, criteria);
}
async getAllStates(): Promise<any> {
return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`));
}

View File

@ -87,7 +87,6 @@ export function createEmptyBusinessListingCriteria(): BusinessListingCriteria {
start: 0,
length: 0,
page: 0,
pageCount: 0,
state: '',
city: '',
types: [],
@ -117,7 +116,6 @@ export function createEmptyCommercialPropertyListingCriteria(): CommercialProper
start: 0,
length: 0,
page: 0,
pageCount: 0,
state: '',
city: '',
types: [],
@ -135,7 +133,6 @@ export function createEmptyUserListingCriteria(): UserListingCriteria {
start: 0,
length: 0,
page: 0,
pageCount: 0,
city: '',
types: [],
prompt: '',
@ -288,3 +285,78 @@ export function initFlowbiteFix() {
}
});
}
// export function arraysEqual(arr1: any[], arr2: any[]): boolean {
// if (arr1.length !== arr2.length) return false;
// for (let i = 0; i < arr1.length; i++) {
// if (arr1[i] !== arr2[i]) return false;
// }
// return true;
// }
export function compareObjects<T extends object>(obj1: T, obj2: T, ignoreProperties: (keyof T)[] = []): number {
let differences = 0;
const keys = Object.keys(obj1) as Array<keyof T>;
for (const key of keys) {
if (ignoreProperties.includes(key)) {
continue; // Überspringe diese Eigenschaft, wenn sie in der Ignore-Liste ist
}
const value1 = obj1[key];
const value2 = obj2[key];
if (!areValuesEqual(value1, value2)) {
differences++;
}
}
return differences;
}
function areValuesEqual(value1: any, value2: any): boolean {
if (Array.isArray(value1) || Array.isArray(value2)) {
return arraysEqual(value1, value2);
}
if (typeof value1 === 'string' || typeof value2 === 'string') {
return isEqualString(value1, value2);
}
if (typeof value1 === 'number' || typeof value2 === 'number') {
return isEqualNumber(value1, value2);
}
if (typeof value1 === 'boolean' || typeof value2 === 'boolean') {
return isEqualBoolean(value1, value2);
}
return value1 === value2;
}
function isEqualString(value1: any, value2: any): boolean {
const isEmptyOrNullish1 = value1 === undefined || value1 === null || value1 === '';
const isEmptyOrNullish2 = value2 === undefined || value2 === null || value2 === '';
return (isEmptyOrNullish1 && isEmptyOrNullish2) || value1 === value2;
}
function isEqualNumber(value1: any, value2: any): boolean {
const isZeroOrNullish1 = value1 === undefined || value1 === null || value1 === 0;
const isZeroOrNullish2 = value2 === undefined || value2 === null || value2 === 0;
return (isZeroOrNullish1 && isZeroOrNullish2) || value1 === value2;
}
function isEqualBoolean(value1: any, value2: any): boolean {
const isFalseOrNullish1 = value1 === undefined || value1 === null || value1 === false;
const isFalseOrNullish2 = value2 === undefined || value2 === null || value2 === false;
return (isFalseOrNullish1 && isFalseOrNullish2) || value1 === value2;
}
function arraysEqual(arr1: any[] | null | undefined, arr2: any[] | null | undefined): boolean {
if (arr1 === arr2) return true;
if (arr1 == null || arr2 == null) return false;
if (arr1.length !== arr2.length) return false;
for (let i = 0; i < arr1.length; i++) {
if (!areValuesEqual(arr1[i], arr2[i])) return false;
}
return true;
}