diff --git a/bizmatch-server/src/app.module.ts b/bizmatch-server/src/app.module.ts index 42188ea..9f36b52 100644 --- a/bizmatch-server/src/app.module.ts +++ b/bizmatch-server/src/app.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller.js'; import { AppService } from './app.service.js'; -import { ListingsController } from './listings/listings.controller.js'; import { FileService } from './file/file.service.js'; import { AuthService } from './auth/auth.service.js'; import { AuthController } from './auth/auth.controller.js'; @@ -25,6 +24,7 @@ import { UserModule } from './user/user.module.js'; import { ListingsModule } from './listings/listings.module.js'; import { AccountModule } from './account/account.module.js'; import { SelectOptionsModule } from './select-options/select-options.module.js'; +import { CommercialPropertyListingsController } from './listings/commercial-property-listings.controller.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/bizmatch-server/src/listings/listings.controller.ts b/bizmatch-server/src/listings/business-listings.controller.ts similarity index 62% rename from bizmatch-server/src/listings/listings.controller.ts rename to bizmatch-server/src/listings/business-listings.controller.ts index 6c9fe3c..76d733a 100644 --- a/bizmatch-server/src/listings/listings.controller.ts +++ b/bizmatch-server/src/listings/business-listings.controller.ts @@ -1,25 +1,23 @@ import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common'; import { FileService } from '../file/file.service.js'; import { convertStringToNullUndefined } from '../utils.js'; -import { RedisService } from '../redis/redis.service.js'; import { ListingsService } from './listings.service.js'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; -@Controller('listings') -export class ListingsController { - // private readonly logger = new Logger(ListingsController.name); - +@Controller('business-listings') +export class BusinessListingsController { + constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { } @Get() findAll(): any { - return this.listingsService.getAllListings(); + return this.listingsService.getAllBusinessListings(); } @Get(':id') findById(@Param('id') id:string): any { - return this.listingsService.getListingById(id); + return this.listingsService.getBusinessListingById(id); } // @Get(':type/:location/:minPrice/:maxPrice/:realEstateChecked') // find(@Param('type') type:string,@Param('location') location:string,@Param('minPrice') minPrice:string,@Param('maxPrice') maxPrice:string,@Param('realEstateChecked') realEstateChecked:boolean): any { @@ -27,24 +25,16 @@ export class ListingsController { // } @Post('search') find(@Body() criteria: any): any { - return this.listingsService.find(criteria); - } - /** - * @param listing updates a new listing - */ - @Put(':id') - updateById(@Param('id') id:string, @Body() listing: any){ - this.logger.info(`Update by ID: ${id}`); - this.listingsService.setListing(listing,id) + return this.listingsService.findBusinessListings(criteria); } /** * @param listing creates a new listing */ @Post() - create(@Body() listing: any){ - this.logger.info(`Create Listing`); - this.listingsService.setListing(listing) + save(@Body() listing: any){ + this.logger.info(`Save Listing`); + this.listingsService.saveListing(listing) } /** @@ -52,7 +42,7 @@ export class ListingsController { */ @Delete(':id') deleteById(@Param('id') id:string){ - this.listingsService.deleteListing(id) + this.listingsService.deleteBusinessListing(id) } } diff --git a/bizmatch-server/src/listings/commercial-property-listings.controller.ts b/bizmatch-server/src/listings/commercial-property-listings.controller.ts new file mode 100644 index 0000000..625c820 --- /dev/null +++ b/bizmatch-server/src/listings/commercial-property-listings.controller.ts @@ -0,0 +1,44 @@ +import { Body, Controller, Delete, Get, Inject, Param, Post } from '@nestjs/common'; +import { ListingsService } from './listings.service.js'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; +import { Logger } from 'winston'; + +@Controller('commercial-property-listings') +export class CommercialPropertyListingsController { + + constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { + } + + @Get() + findAll(): any { + return this.listingsService.getAllCommercialListings(); + } + + @Get(':id') + findById(@Param('id') id:string): any { + return this.listingsService.getCommercialPropertyListingById(id); + } + + @Post('search') + find(@Body() criteria: any): any { + return this.listingsService.findCommercialPropertyListings(criteria); + } + + /** + * @param listing creates a new listing + */ + @Post() + save(@Body() listing: any){ + this.logger.info(`Save Listing`); + this.listingsService.saveListing(listing) + } + + /** + * @param id deletes a listing + */ + @Delete(':id') + deleteById(@Param('id') id:string){ + this.listingsService.deleteCommercialPropertyListing(id) + } + +} diff --git a/bizmatch-server/src/listings/listings.module.ts b/bizmatch-server/src/listings/listings.module.ts index 4a211c5..1cb3b21 100644 --- a/bizmatch-server/src/listings/listings.module.ts +++ b/bizmatch-server/src/listings/listings.module.ts @@ -1,9 +1,12 @@ import { Module } from '@nestjs/common'; -import { ListingsController } from './listings.controller.js'; +import { BusinessListingsController } from './business-listings.controller.js'; import { ListingsService } from './listings.service.js'; +import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js'; +import { RedisModule } from '../redis/redis.module.js'; @Module({ - controllers: [ListingsController], + imports: [RedisModule], + controllers: [BusinessListingsController, CommercialPropertyListingsController], providers: [ListingsService] }) export class ListingsModule {} diff --git a/bizmatch-server/src/listings/listings.service.ts b/bizmatch-server/src/listings/listings.service.ts index 703765a..77bd01e 100644 --- a/bizmatch-server/src/listings/listings.service.ts +++ b/bizmatch-server/src/listings/listings.service.ts @@ -1,65 +1,118 @@ import { Inject, Injectable } from '@nestjs/common'; import { BusinessListing, - InvestmentsListing, + CommercialPropertyListing, ListingCriteria, - ProfessionalsBrokersListing, + ListingType } from '../models/main.model.js'; import { convertStringToNullUndefined } from '../utils.js'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; +import { EntityData, EntityId, Repository, Schema, SchemaDefinition } from 'redis-om'; +import { REDIS_CLIENT } from '../redis/redis.module.js'; @Injectable() export class ListingsService { - constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {} - - async setListing( - value: BusinessListing | ProfessionalsBrokersListing | InvestmentsListing, - id?: string, - ) { - // if (!id) { - // id = await this.redisService.getId(LISTINGS); - // value.id = id; - // this.logger.info(`No ID - creating new one:${id}`) - // } else { - // this.logger.info(`ID available:${id}`) - // } - //this.redisService.setJson(id, value); + businessListingRepository:Repository; + commercialPropertyListingRepository:Repository; + baseListingSchemaDef : SchemaDefinition = { + id: { type: 'string' }, + userId: { type: 'string' }, + listingsCategory: { type: 'string' }, + title: { type: 'string' }, + description: { type: 'string' }, + country: { type: 'string' }, + state:{ type: 'string' }, + city:{ type: 'string' }, + zipCode: { type: 'number' }, + type: { type: 'string' }, + price: { type: 'number' }, + favoritesForUser:{ type: 'string[]' }, + hideImage:{ type: 'boolean' }, + draft:{ type: 'boolean' }, + created:{ type: 'date' }, + updated:{ type: 'date' } + } + businessListingSchemaDef : SchemaDefinition = { + ...this.baseListingSchemaDef, + salesRevenue: { type: 'number' }, + cashFlow: { type: 'number' }, + employees: { type: 'number' }, + established: { type: 'number' }, + internalListingNumber: { type: 'number' }, + realEstateIncluded:{ type: 'boolean' }, + leasedLocation:{ type: 'boolean' }, + franchiseResale:{ type: 'boolean' }, + supportAndTraining: { type: 'string' }, + reasonForSale: { type: 'string' }, + brokerLicencing: { type: 'string' }, + internals: { type: 'string' }, + } + commercialPropertyListingSchemaDef : SchemaDefinition = { + ...this.baseListingSchemaDef, + imageNames:{ type: 'string[]' }, + } + businessListingSchema = new Schema('businessListing',this.businessListingSchemaDef, { + dataStructure: 'JSON' + }) + commercialPropertyListingSchema = new Schema('commercialPropertyListing',this.commercialPropertyListingSchemaDef, { + dataStructure: 'JSON' + }) + constructor(@Inject(REDIS_CLIENT) private readonly redis: any){ + this.businessListingRepository = new Repository(this.businessListingSchema, redis); + this.commercialPropertyListingRepository = new Repository(this.commercialPropertyListingSchema, redis) + this.businessListingRepository.createIndex(); + this.commercialPropertyListingRepository.createIndex(); + } + async saveListing(listing: BusinessListing | CommercialPropertyListing) { + const repo=listing.listingsCategory==='business'?this.businessListingRepository:this.commercialPropertyListingRepository; + let result + if (listing.id){ + result = await repo.save(listing.id,listing as any) + } else { + result = await repo.save(listing as any) + listing.id=result[EntityId]; + result = await repo.save(listing.id,listing as any) + } + return result; + } + async getCommercialPropertyListingById(id: string) { + return await this.commercialPropertyListingRepository.fetch(id) + } + async getBusinessListingById(id: string) { + return await this.businessListingRepository.fetch(id) + } + async deleteBusinessListing(id: string){ + return await this.businessListingRepository.remove(id); + } + async deleteCommercialPropertyListing(id: string){ + return await this.commercialPropertyListingRepository.remove(id); + } + async getAllBusinessListings(start?: number, end?: number) { + return await this.businessListingRepository.search().return.all() + } + async getAllCommercialListings(start?: number, end?: number) { + return await this.commercialPropertyListingRepository.search().return.all() } - async getListingById(id: string) { - //return await this.redisService.getJson(id, LISTINGS); + + async findBusinessListings(criteria:ListingCriteria): Promise { + let listings = await this.getAllBusinessListings(); + return this.find(criteria,listings); } - deleteListing(id: string){ - //this.redisService.delete(id); - this.logger.info(`delete listing with ID:${id}`) + async findCommercialPropertyListings(criteria:ListingCriteria): Promise { + let listings = await this.getAllCommercialListings(); + return this.find(criteria,listings); } - async getAllListings(start?: number, end?: number) { - // const searchResult = await this.redisService.search(LISTINGS, '*'); - // const listings = searchResult.slice(1).reduce((acc, item, index, array) => { - // if (index % 2 === 1) { - // try { - // const listing = JSON.parse(item[1]); - // acc.push(listing); - // } catch (error) { - // console.error('Fehler beim Parsen des JSON-Strings: ', error); - // } - // } - // return acc; - // }, []); - // return listings; - return []; - } - async find(criteria:ListingCriteria): Promise { - let listings = await this.getAllListings(); + async find(criteria:ListingCriteria, listings: any[]): Promise { listings=listings.filter(l=>l.listingsCategory===criteria.listingsCategory); if (convertStringToNullUndefined(criteria.type)){ console.log(criteria.type); listings=listings.filter(l=>l.type===criteria.type); } - if (convertStringToNullUndefined(criteria.location)){ - console.log(criteria.location); - listings=listings.filter(l=>l.location===criteria.location); + if (convertStringToNullUndefined(criteria.state)){ + console.log(criteria.state); + listings=listings.filter(l=>l.state===criteria.state); } if (convertStringToNullUndefined(criteria.minPrice)){ console.log(criteria.minPrice); diff --git a/bizmatch-server/src/redis/redis.controller.ts b/bizmatch-server/src/redis/redis.controller.ts deleted file mode 100644 index 5020a3f..0000000 --- a/bizmatch-server/src/redis/redis.controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Post, Res } from '@nestjs/common'; -import { Response } from 'express'; -import { RedisService } from './redis.service.js'; - -@Controller('redis') -export class RedisController { - constructor(private redisService:RedisService){} - -} diff --git a/bizmatch-server/src/redis/redis.module.ts b/bizmatch-server/src/redis/redis.module.ts index 5d6cf3b..e665ce8 100644 --- a/bizmatch-server/src/redis/redis.module.ts +++ b/bizmatch-server/src/redis/redis.module.ts @@ -24,8 +24,6 @@ export class RedisModule {} export const REDIS_CLIENT = "REDIS_CLIENT"; // redis.service.ts import { Injectable } from '@nestjs/common'; -import { RedisService } from './redis.service.js'; -import { RedisController } from './redis.controller.js'; import { createClient } from 'redis'; diff --git a/bizmatch-server/src/redis/redis.service.ts b/bizmatch-server/src/redis/redis.service.ts deleted file mode 100644 index 8874f22..0000000 --- a/bizmatch-server/src/redis/redis.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -// redis.service.ts -import { Injectable } from '@nestjs/common'; -import { BusinessListing, InvestmentsListing,ProfessionalsBrokersListing } from '../models/main.model.js'; -import fs from 'fs-extra'; -import { createClient } from 'redis'; - -export const LISTINGS = 'LISTINGS'; -export const SUBSCRIPTIONS = 'SUBSCRIPTIONS'; -export const USERS = 'USERS' -export const redis = createClient({ url: 'redis://localhost:6379' }) -@Injectable() -export class RedisService { - //private redis = new Redis(); // Verbindungsparameter nach Bedarf anpassen - // private redis = new Redis({ - // port: 6379, // Der TLS-Port von Redis - //host: '2.56.188.138', - // host: '127.0.0.1', - //password: 'bizmatchRedis:5600Wuppertal11', // ACL Benutzername und Passwort - // tls: { - // key: fs.readFileSync('/home/aknuth/ssl/private/redis.key'), - // cert: fs.readFileSync('/home/aknuth/ssl/certs/redis.crt') - // } - // }); - - - - // ###################################### - // general methods - // ###################################### - // async setJson(id: string, value: any): Promise { - // await this.redis.call("JSON.SET", `${LISTINGS}:${id}`, "$", JSON.stringify(value)); - // } - // async delete(id: string): Promise { - // await this.redis.del(`${LISTINGS}:${id}`); - // } - // async getJson(id: string, prefix:string): Promise { - // const result:string = await this.redis.call("JSON.GET", `${prefix}:${id}`) as string; - // return JSON.parse(result); - // } - - // async getId(prefix:'LISTINGS'|'SUBSCRIPTIONS'|'USERS'):Promise{ - // const counter = await this.redis.call("INCR",`${prefix}_ID_COUNTER`) as number; - // return counter.toString().padStart(15, '0') - // } - - // async search(prefix:'LISTINGS'|'SUBSCRIPTIONS'|'USERS',clause:string):Promise{ - // const result = await this.redis.call(`FT.SEARCH`, `${prefix}_INDEX`, `${clause}`, 'LIMIT', 0, 200); - // return result; - // } -} \ No newline at end of file diff --git a/bizmatch-server/src/select-options/select-options.service.ts b/bizmatch-server/src/select-options/select-options.service.ts index 9a89fff..6013d01 100644 --- a/bizmatch-server/src/select-options/select-options.service.ts +++ b/bizmatch-server/src/select-options/select-options.service.ts @@ -30,7 +30,7 @@ export class SelectOptionsService { public listingCategories: Array = [ { name: 'Business', value: 'business' }, // { name: 'Professionals/Brokers Directory', value: 'professionals_brokers' }, - { name: 'Investment Property', value: 'investment' }, + { name: 'Commercial Property', value: 'commercialProperty' }, ] public categories: Array = [ { name: 'Broker', value: 'broker', icon:'pi-image',bgColorClass:'bg-green-100',textColorClass:'text-green-600' }, diff --git a/bizmatch-server/src/user/user.service.ts b/bizmatch-server/src/user/user.service.ts index c7123da..c5d0563 100644 --- a/bizmatch-server/src/user/user.service.ts +++ b/bizmatch-server/src/user/user.service.ts @@ -4,13 +4,12 @@ import { Entity, Repository, Schema } from 'redis-om'; import { User } from '../models/main.model.js'; import { REDIS_CLIENT } from '../redis/redis.module.js'; import { UserEntity } from '../models/server.model.js'; -// export const redis = createClient({ url: 'redis://localhost:6379' }) @Injectable() export class UserService { userRepository:Repository; userSchema = new Schema('user',{ - // id: string; + id: { type: 'string' }, firstname: { type: 'string' }, lastname: { type: 'string' }, email: { type: 'string' }, @@ -25,22 +24,14 @@ export class UserService { }, { dataStructure: 'JSON' }) - constructor(@Inject(REDIS_CLIENT) private readonly client: any){ - // const redis = createClient({ url: 'redis://localhost:6379' }) - this.userRepository = new Repository(this.userSchema, client) + constructor(@Inject(REDIS_CLIENT) private readonly redis: any){ + this.userRepository = new Repository(this.userSchema, redis) } - async getUserById( id:string){ return await this.userRepository.fetch(id); } async saveUser(user:any):Promise{ return await this.userRepository.save(user.id,user) as UserEntity } - // createUser(){ - - // } - - // updateById(id:string){ - - // } + } \ No newline at end of file diff --git a/bizmatch/package.json b/bizmatch/package.json index bcb9992..891f92e 100644 --- a/bizmatch/package.json +++ b/bizmatch/package.json @@ -36,7 +36,7 @@ "on-change": "^5.0.1", "primeflex": "^3.3.1", "primeicons": "^6.0.1", - "primeng": "^17.6.0", + "primeng": "^17.10.0", "rxjs": "~7.8.1", "tslib": "^2.3.0", "urlcat": "^3.1.0", diff --git a/bizmatch/src/app/app.config.ts b/bizmatch/src/app/app.config.ts index bafde0a..2059a99 100644 --- a/bizmatch/src/app/app.config.ts +++ b/bizmatch/src/app/app.config.ts @@ -1,4 +1,4 @@ -import { APP_INITIALIZER, ApplicationConfig, importProvidersFrom } from '@angular/core'; +import { APP_INITIALIZER, ApplicationConfig, LOCALE_ID, importProvidersFrom } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; @@ -26,7 +26,8 @@ export const appConfig: ApplicationConfig = { multi: true, deps: [SelectOptionsService], }, - provideRouter(routes),provideAnimations() + provideRouter(routes),provideAnimations(), + // {provide: LOCALE_ID, useValue: 'en-US' } ] }; function initUserService(userService:UserService) { diff --git a/bizmatch/src/app/components/header/header.component.ts b/bizmatch/src/app/components/header/header.component.ts index 3bccb1a..ef47ee0 100644 --- a/bizmatch/src/app/components/header/header.component.ts +++ b/bizmatch/src/app/components/header/header.component.ts @@ -45,8 +45,8 @@ export class HeaderComponent { fragment:'' }, { - label: 'Investment Property', - routerLink: '/listings/investment', + label: 'Commercial Property', + routerLink: '/listings/commercialProperty', fragment:'' } ]; diff --git a/bizmatch/src/app/components/inputnumber/inputnumber.component.scss b/bizmatch/src/app/components/inputnumber/inputnumber.component.scss new file mode 100644 index 0000000..4f543e4 --- /dev/null +++ b/bizmatch/src/app/components/inputnumber/inputnumber.component.scss @@ -0,0 +1,115 @@ +// @layer primeng { + app-inputnumber, + .p-inputnumber { + display: inline-flex; + } + + .p-inputnumber-button { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + } + + .p-inputnumber-buttons-stacked .p-button.p-inputnumber-button .p-button-label, + .p-inputnumber-buttons-horizontal .p-button.p-inputnumber-button .p-button-label { + display: none; + } + + .p-inputnumber-buttons-stacked .p-button.p-inputnumber-button-up { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + padding: 0; + } + + .p-inputnumber-buttons-stacked .p-inputnumber-input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .p-inputnumber-buttons-stacked .p-button.p-inputnumber-button-down { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: 0; + padding: 0; + } + + .p-inputnumber-buttons-stacked .p-inputnumber-button-group { + display: flex; + flex-direction: column; + } + + .p-inputnumber-buttons-stacked .p-inputnumber-button-group .p-button.p-inputnumber-button { + flex: 1 1 auto; + } + + .p-inputnumber-buttons-horizontal .p-button.p-inputnumber-button-up { + order: 3; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + .p-inputnumber-buttons-horizontal .p-inputnumber-input { + order: 2; + border-radius: 0; + } + + .p-inputnumber-buttons-horizontal .p-button.p-inputnumber-button-down { + order: 1; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .p-inputnumber-buttons-vertical { + flex-direction: column; + } + + .p-inputnumber-buttons-vertical .p-button.p-inputnumber-button-up { + order: 1; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + width: 100%; + } + + .p-inputnumber-buttons-vertical .p-inputnumber-input { + order: 2; + border-radius: 0; + text-align: center; + } + + .p-inputnumber-buttons-vertical .p-button.p-inputnumber-button-down { + order: 3; + border-top-left-radius: 0; + border-top-right-radius: 0; + width: 100%; + } + + .p-inputnumber-input { + flex: 1 1 auto; + } + + .p-fluid app-inputnumber, + .p-fluid .p-inputnumber { + width: 100%; + } + + .p-fluid .p-inputnumber .p-inputnumber-input { + width: 1%; + } + + .p-fluid .p-inputnumber-buttons-vertical .p-inputnumber-input { + width: 100%; + } + + .p-inputnumber-clear-icon { + position: absolute; + top: 50%; + margin-top: -0.5rem; + cursor: pointer; + } + + .p-inputnumber-clearable { + position: relative; + } +// } \ No newline at end of file diff --git a/bizmatch/src/app/components/inputnumber/inputnumber.component.ts b/bizmatch/src/app/components/inputnumber/inputnumber.component.ts new file mode 100644 index 0000000..dfae10f --- /dev/null +++ b/bizmatch/src/app/components/inputnumber/inputnumber.component.ts @@ -0,0 +1,1480 @@ +import { CommonModule, DOCUMENT } from '@angular/common'; +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + Inject, + Injector, + Input, + NgModule, + OnChanges, + OnInit, + Output, + QueryList, + SimpleChanges, + TemplateRef, + ViewChild, + ViewEncapsulation, + forwardRef +} from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms'; +import { PrimeTemplate, SharedModule } from 'primeng/api'; +import { ButtonModule } from 'primeng/button'; +import { DomHandler } from 'primeng/dom'; +import { AngleDownIcon } from 'primeng/icons/angledown'; +import { AngleUpIcon } from 'primeng/icons/angleup'; +import { TimesIcon } from 'primeng/icons/times'; +import { InputTextModule } from 'primeng/inputtext'; +import { Nullable } from 'primeng/ts-helpers'; + +/** + * Custom input event. + * @see {@link InputNumber.onInput} + * @group Events + */ +export interface InputNumberInputEvent { + /** + * Browser event. + */ + originalEvent: Event; + /** + * Input value. + */ + value: number; + /** + * Selected option value. + */ + formattedValue: string; +} + +/** + * Defines valid templates in InputNumber. + * @group Templates + */ +export interface InputNumberTemplates { + /** + * Custom clear icon template. + */ + clearicon(): TemplateRef; + /** + * Custom increment button icon template. + */ + incrementbuttonicon(): TemplateRef; + /** + * Custom decrement button icon template. + */ + decrementbuttonicon(): TemplateRef; +} +export const INPUTNUMBER_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => InputNumberComponent), + multi: true +}; +/** + * InputNumber is an input component to provide numerical input. + * @group Components + */ +@Component({ + selector: 'app-inputNumber', + template: ` + + + + + + + + + + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [INPUTNUMBER_VALUE_ACCESSOR], + encapsulation: ViewEncapsulation.None, + styleUrl: './inputnumber.component.scss', + host: { + class: 'p-element p-inputwrapper', + '[class.p-inputwrapper-filled]': 'filled', + '[class.p-inputwrapper-focus]': 'focused', + '[class.p-inputnumber-clearable]': 'showClear && buttonLayout != "vertical"' + } +}) +export class InputNumberComponent implements OnInit, AfterContentInit, OnChanges, ControlValueAccessor { + /** + * Displays spinner buttons. + * @group Props + */ + @Input() showButtons: boolean = false; + /** + * Whether to format the value. + * @group Props + */ + @Input() format: boolean = true; + /** + * Layout of the buttons, valid values are "stacked" (default), "horizontal" and "vertical". + * @group Props + */ + @Input() buttonLayout: string = 'stacked'; + /** + * Identifier of the focus input to match a label defined for the component. + * @group Props + */ + @Input() inputId: string | undefined; + /** + * Style class of the component. + * @group Props + */ + @Input() styleClass: string | undefined; + /** + * Inline style of the component. + * @group Props + */ + @Input() style: { [klass: string]: any } | null | undefined; + /** + * Advisory information to display on input. + * @group Props + */ + @Input() placeholder: string | undefined; + /** + * Size of the input field. + * @group Props + */ + @Input() size: number | undefined; + /** + * Maximum number of character allows in the input field. + * @group Props + */ + @Input() maxlength: number | undefined; + /** + * Specifies tab order of the element. + * @group Props + */ + @Input() tabindex: number | undefined; + /** + * Title text of the input text. + * @group Props + */ + @Input() title: string | undefined; + /** + * Specifies one or more IDs in the DOM that labels the input field. + * @group Props + */ + @Input() ariaLabelledBy: string | undefined; + /** + * Used to define a string that labels the input element. + * @group Props + */ + @Input() ariaLabel: string | undefined; + /** + * Used to indicate that user input is required on an element before a form can be submitted. + * @group Props + */ + @Input() ariaRequired: boolean | undefined; + /** + * Name of the input field. + * @group Props + */ + @Input() name: string | undefined; + /** + * Indicates that whether the input field is required. + * @group Props + */ + @Input() required: boolean | undefined; + /** + * Used to define a string that autocomplete attribute the current element. + * @group Props + */ + @Input() autocomplete: string | undefined; + /** + * Mininum boundary value. + * @group Props + */ + @Input() min: number | undefined; + /** + * Maximum boundary value. + * @group Props + */ + @Input() max: number | undefined; + /** + * Style class of the increment button. + * @group Props + */ + @Input() incrementButtonClass: string | undefined; + /** + * Style class of the decrement button. + * @group Props + */ + @Input() decrementButtonClass: string | undefined; + /** + * Style class of the increment button. + * @group Props + */ + @Input() incrementButtonIcon: string | undefined; + /** + * Style class of the decrement button. + * @group Props + */ + @Input() decrementButtonIcon: string | undefined; + /** + * When present, it specifies that an input field is read-only. + * @group Props + */ + @Input() readonly: boolean = false; + /** + * Step factor to increment/decrement the value. + * @group Props + */ + @Input() step: number = 1; + /** + * Determines whether the input field is empty. + * @group Props + */ + @Input() allowEmpty: boolean = true; + /** + * Locale to be used in formatting. + * @group Props + */ + @Input() locale: string | undefined; + /** + * The locale matching algorithm to use. Possible values are "lookup" and "best fit"; the default is "best fit". See Locale Negotiation for details. + * @group Props + */ + @Input() localeMatcher: string | undefined; + /** + * Defines the behavior of the component, valid values are "decimal" and "currency". + * @group Props + */ + @Input() mode: string = 'decimal'; + /** + * The currency to use in currency formatting. Possible values are the ISO 4217 currency codes, such as "USD" for the US dollar, "EUR" for the euro, or "CNY" for the Chinese RMB. There is no default value; if the style is "currency", the currency property must be provided. + * @group Props + */ + @Input() currency: string | undefined; + /** + * How to display the currency in currency formatting. Possible values are "symbol" to use a localized currency symbol such as €, ü"code" to use the ISO currency code, "name" to use a localized currency name such as "dollar"; the default is "symbol". + * @group Props + */ + @Input() currencyDisplay: string | undefined; + /** + * Whether to use grouping separators, such as thousands separators or thousand/lakh/crore separators. + * @group Props + */ + @Input() useGrouping: boolean = true; + /** + * The minimum number of fraction digits to use. Possible values are from 0 to 20; the default for plain number and percent formatting is 0; the default for currency formatting is the number of minor unit digits provided by the ISO 4217 currency code list (2 if the list doesn't provide that information). + * @group Props + */ + @Input() minFractionDigits: number | undefined; + /** + * The maximum number of fraction digits to use. Possible values are from 0 to 20; the default for plain number formatting is the larger of minimumFractionDigits and 3; the default for currency formatting is the larger of minimumFractionDigits and the number of minor unit digits provided by the ISO 4217 currency code list (2 if the list doesn't provide that information). + * @group Props + */ + @Input() maxFractionDigits: number | undefined; + /** + * Text to display before the value. + * @group Props + */ + @Input() prefix: string | undefined; + /** + * Text to display after the value. + * @group Props + */ + @Input() suffix: string | undefined; + /** + * Inline style of the input field. + * @group Props + */ + @Input() inputStyle: any; + /** + * Style class of the input field. + * @group Props + */ + @Input() inputStyleClass: string | undefined; + /** + * When enabled, a clear icon is displayed to clear the value. + * @group Props + */ + @Input() showClear: boolean = false; + /** + * When present, it specifies that the element should be disabled. + * @group Props + */ + @Input() get disabled(): boolean | undefined { + return this._disabled; + } + set disabled(disabled: boolean | undefined) { + if (disabled) this.focused = false; + + this._disabled = disabled; + + if (this.timer) this.clearTimer(); + } + /** + * Callback to invoke on input. + * @param {InputNumberInputEvent} event - Custom input event. + * @group Emits + */ + @Output() onInput: EventEmitter = new EventEmitter(); + /** + * Callback to invoke when the component receives focus. + * @param {Event} event - Browser event. + * @group Emits + */ + @Output() onFocus: EventEmitter = new EventEmitter(); + /** + * Callback to invoke when the component loses focus. + * @param {Event} event - Browser event. + * @group Emits + */ + @Output() onBlur: EventEmitter = new EventEmitter(); + /** + * Callback to invoke on input key press. + * @param {KeyboardEvent} event - Keyboard event. + * @group Emits + */ + @Output() onKeyDown: EventEmitter = new EventEmitter(); + /** + * Callback to invoke when clear token is clicked. + * @group Emits + */ + @Output() onClear: EventEmitter = new EventEmitter(); + + @ViewChild('input') input!: ElementRef; + + @ContentChildren(PrimeTemplate) templates!: QueryList; + + clearIconTemplate: Nullable>; + + incrementButtonIconTemplate: Nullable>; + + decrementButtonIconTemplate: Nullable>; + + value: Nullable; + + onModelChange: Function = () => {}; + + onModelTouched: Function = () => {}; + + focused: Nullable; + + initialized: Nullable; + + groupChar: string = ''; + + prefixChar: string = ''; + + suffixChar: string = ''; + + isSpecialChar: Nullable; + + timer: any; + + lastValue: Nullable; + + _numeral: any; + + numberFormat: any; + + _decimal: any; + + _decimalChar: string; + + _group: any; + + _minusSign: any; + + _currency: Nullable; + + _prefix: Nullable; + + _suffix: Nullable; + + _index: number | any; + + _disabled: boolean | undefined; + + private ngControl: NgControl | null = null; + + constructor(@Inject(DOCUMENT) private document: Document, public el: ElementRef, private cd: ChangeDetectorRef, private readonly injector: Injector) {} + + ngOnChanges(simpleChange: SimpleChanges) { + const props = ['locale', 'localeMatcher', 'mode', 'currency', 'currencyDisplay', 'useGrouping', 'minFractionDigits', 'maxFractionDigits', 'prefix', 'suffix']; + if (props.some((p) => !!simpleChange[p])) { + this.updateConstructParser(); + } + } + + ngAfterContentInit() { + this.templates.forEach((item) => { + switch (item.getType()) { + case 'clearicon': + this.clearIconTemplate = item.template; + break; + + case 'incrementbuttonicon': + this.incrementButtonIconTemplate = item.template; + break; + + case 'decrementbuttonicon': + this.decrementButtonIconTemplate = item.template; + break; + } + }); + } + + ngOnInit() { + this.ngControl = this.injector.get(NgControl, null, { optional: true }); + + this.constructParser(); + + this.initialized = true; + } + + getOptions() { + return { + localeMatcher: this.localeMatcher, + style: this.mode, + currency: this.currency, + currencyDisplay: this.currencyDisplay, + useGrouping: this.useGrouping, + minimumFractionDigits: this.minFractionDigits, + maximumFractionDigits: this.maxFractionDigits + }; + } + + constructParser() { + this.numberFormat = new Intl.NumberFormat(this.locale, this.getOptions()); + const numerals = [...new Intl.NumberFormat(this.locale, { useGrouping: false }).format(9876543210)].reverse(); + const index = new Map(numerals.map((d, i) => [d, i])); + this._numeral = new RegExp(`[${numerals.join('')}]`, 'g'); + this._group = this.getGroupingExpression(); + this._minusSign = this.getMinusSignExpression(); + this._currency = this.getCurrencyExpression(); + this._decimal = this.getDecimalExpression(); + this._decimalChar = this.getDecimalChar(); + this._suffix = this.getSuffixExpression(); + this._prefix = this.getPrefixExpression(); + this._index = (d: any) => index.get(d); + } + + updateConstructParser() { + if (this.initialized) { + this.constructParser(); + } + } + + escapeRegExp(text: string): string { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + } + + getDecimalExpression(): RegExp { + const decimalChar = this.getDecimalChar(); + return new RegExp(`[${decimalChar}]`, 'g'); + } + getDecimalChar(): string { + const formatter = new Intl.NumberFormat(this.locale, { ...this.getOptions(), useGrouping: false }); + return formatter + .format(1.1) + .replace(this._currency as RegExp | string, '') + .trim() + .replace(this._numeral, ''); + } + + getGroupingExpression(): RegExp { + const formatter = new Intl.NumberFormat(this.locale, { useGrouping: true }); + this.groupChar = formatter.format(1000000).trim().replace(this._numeral, '').charAt(0); + return new RegExp(`[${this.groupChar}]`, 'g'); + } + + getMinusSignExpression(): RegExp { + const formatter = new Intl.NumberFormat(this.locale, { useGrouping: false }); + return new RegExp(`[${formatter.format(-1).trim().replace(this._numeral, '')}]`, 'g'); + } + + getCurrencyExpression(): RegExp { + if (this.currency) { + const formatter = new Intl.NumberFormat(this.locale, { style: 'currency', currency: this.currency, currencyDisplay: this.currencyDisplay, minimumFractionDigits: 0, maximumFractionDigits: 0 }); + return new RegExp(`[${formatter.format(1).replace(/\s/g, '').replace(this._numeral, '').replace(this._group, '')}]`, 'g'); + } + + return new RegExp(`[]`, 'g'); + } + + getPrefixExpression(): RegExp { + if (this.prefix) { + this.prefixChar = this.prefix; + } else { + const formatter = new Intl.NumberFormat(this.locale, { style: this.mode, currency: this.currency, currencyDisplay: this.currencyDisplay }); + this.prefixChar = formatter.format(1).split('1')[0]; + } + + return new RegExp(`${this.escapeRegExp(this.prefixChar || '')}`, 'g'); + } + + getSuffixExpression(): RegExp { + if (this.suffix) { + this.suffixChar = this.suffix; + } else { + const formatter = new Intl.NumberFormat(this.locale, { style: this.mode, currency: this.currency, currencyDisplay: this.currencyDisplay, minimumFractionDigits: 0, maximumFractionDigits: 0 }); + this.suffixChar = formatter.format(1).split('1')[1]; + } + + return new RegExp(`${this.escapeRegExp(this.suffixChar || '')}`, 'g'); + } + + formatValue(value: any) { + if (value != null) { + if (value === '-') { + // Minus sign + return value; + } + + if (this.format) { + let formatter = new Intl.NumberFormat(this.locale, this.getOptions()); + let formattedValue = formatter.format(value); + if (this.prefix) { + formattedValue = this.prefix + formattedValue; + } + + if (this.suffix) { + formattedValue = formattedValue + this.suffix; + } + + return formattedValue; + } + + return value.toString(); + } + + return ''; + } + + parseValue(text: any) { + let filteredText = text + .replace(this._suffix as RegExp, '') + .replace(this._prefix as RegExp, '') + .trim() + .replace(/\s/g, '') + .replace(this._currency as RegExp, '') + .replace(this._group, '') + .replace(this._minusSign, '-') + .replace(this._decimal, '.') + .replace(this._numeral, this._index); + + if (filteredText) { + if (filteredText === '-') + // Minus sign + return filteredText; + + let parsedValue = +filteredText; + return isNaN(parsedValue) ? null : parsedValue; + } + + return null; + } + + repeat(event: Event, interval: number | null, dir: number) { + if (this.readonly) { + return; + } + + let i = interval || 500; + + this.clearTimer(); + this.timer = setTimeout(() => { + this.repeat(event, 40, dir); + }, i); + + this.spin(event, dir); + } + + spin(event: Event, dir: number) { + let step = this.step * dir; + let currentValue = this.parseValue(this.input?.nativeElement.value) || 0; + let newValue = this.validateValue((currentValue as number) + step); + if (this.maxlength && this.maxlength < this.formatValue(newValue).length) { + return; + } + this.updateInput(newValue, null, 'spin', null); + this.updateModel(event, newValue); + + this.handleOnInput(event, currentValue, newValue); + } + + clear() { + this.value = null; + this.onModelChange(this.value); + this.onClear.emit(); + } + + onUpButtonMouseDown(event: MouseEvent) { + if (event.button === 2) { + this.clearTimer(); + return; + } + + if (!this.disabled) { + this.input?.nativeElement.focus(); + this.repeat(event, null, 1); + event.preventDefault(); + } + } + + onUpButtonMouseUp() { + if (!this.disabled) { + this.clearTimer(); + } + } + + onUpButtonMouseLeave() { + if (!this.disabled) { + this.clearTimer(); + } + } + + onUpButtonKeyDown(event: KeyboardEvent) { + if (event.keyCode === 32 || event.keyCode === 13) { + this.repeat(event, null, 1); + } + } + + onUpButtonKeyUp() { + if (!this.disabled) { + this.clearTimer(); + } + } + + onDownButtonMouseDown(event: MouseEvent) { + if (event.button === 2) { + this.clearTimer(); + return; + } + if (!this.disabled) { + this.input?.nativeElement.focus(); + this.repeat(event, null, -1); + event.preventDefault(); + } + } + + onDownButtonMouseUp() { + if (!this.disabled) { + this.clearTimer(); + } + } + + onDownButtonMouseLeave() { + if (!this.disabled) { + this.clearTimer(); + } + } + + onDownButtonKeyUp() { + if (!this.disabled) { + this.clearTimer(); + } + } + + onDownButtonKeyDown(event: KeyboardEvent) { + if (event.keyCode === 32 || event.keyCode === 13) { + this.repeat(event, null, -1); + } + } + + onUserInput(event: Event) { + if (this.readonly) { + return; + } + + if (this.isSpecialChar) { + (event.target as HTMLInputElement).value = this.lastValue as string; + } + this.isSpecialChar = false; + } + + onInputKeyDown(event: KeyboardEvent) { + if (this.readonly) { + return; + } + + this.lastValue = (event.target as HTMLInputElement).value; + if ((event as KeyboardEvent).shiftKey || (event as KeyboardEvent).altKey) { + this.isSpecialChar = true; + return; + } + + let selectionStart = (event.target as HTMLInputElement).selectionStart as number; + let selectionEnd = (event.target as HTMLInputElement).selectionEnd as number; + let inputValue = (event.target as HTMLInputElement).value as string; + let newValueStr = null; + + if (event.altKey) { + event.preventDefault(); + } + + switch (event.code) { + case 'ArrowUp': + this.spin(event, 1); + event.preventDefault(); + break; + + case 'ArrowDown': + this.spin(event, -1); + event.preventDefault(); + break; + + case 'ArrowLeft': + for (let index = selectionStart; index <= inputValue.length; index++) { + const previousCharIndex = index === 0 ? 0 : index - 1; + if (this.isNumeralChar(inputValue.charAt(previousCharIndex))) { + this.input.nativeElement.setSelectionRange(index, index); + break; + } + } + break; + + case 'ArrowRight': + for (let index = selectionEnd; index >= 0; index--) { + if (this.isNumeralChar(inputValue.charAt(index))) { + this.input.nativeElement.setSelectionRange(index, index); + break; + } + } + break; + + case 'Tab': + case 'Enter': + newValueStr = this.validateValue(this.parseValue(this.input.nativeElement.value)); + this.input.nativeElement.value = this.formatValue(newValueStr); + this.input.nativeElement.setAttribute('aria-valuenow', newValueStr); + this.updateModel(event, newValueStr); + break; + + case 'Backspace': { + event.preventDefault(); + + if (selectionStart === selectionEnd) { + const deleteChar = inputValue.charAt(selectionStart - 1); + const { decimalCharIndex, decimalCharIndexWithoutPrefix } = this.getDecimalCharIndexes(inputValue); + + if (this.isNumeralChar(deleteChar)) { + const decimalLength = this.getDecimalLength(inputValue); + + if (this._group.test(deleteChar)) { + this._group.lastIndex = 0; + newValueStr = inputValue.slice(0, selectionStart - 2) + inputValue.slice(selectionStart - 1); + } else if (this._decimal.test(deleteChar)) { + this._decimal.lastIndex = 0; + + if (decimalLength) { + this.input?.nativeElement.setSelectionRange(selectionStart - 1, selectionStart - 1); + } else { + newValueStr = inputValue.slice(0, selectionStart - 1) + inputValue.slice(selectionStart); + } + } else if (decimalCharIndex > 0 && selectionStart > decimalCharIndex) { + const insertedText = this.isDecimalMode() && (this.minFractionDigits || 0) < decimalLength ? '' : '0'; + newValueStr = inputValue.slice(0, selectionStart - 1) + insertedText + inputValue.slice(selectionStart); + } else if (decimalCharIndexWithoutPrefix === 1) { + newValueStr = inputValue.slice(0, selectionStart - 1) + '0' + inputValue.slice(selectionStart); + newValueStr = (this.parseValue(newValueStr) as number) > 0 ? newValueStr : ''; + } else { + newValueStr = inputValue.slice(0, selectionStart - 1) + inputValue.slice(selectionStart); + } + } else if (this.mode === 'currency' && deleteChar.search(this._currency) != -1) { + newValueStr = inputValue.slice(1); + } + + this.updateValue(event, newValueStr, null, 'delete-single'); + } else { + newValueStr = this.deleteRange(inputValue, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, null, 'delete-range'); + } + + break; + } + + case 'Delete': + event.preventDefault(); + + if (selectionStart === selectionEnd) { + const deleteChar = inputValue.charAt(selectionStart); + const { decimalCharIndex, decimalCharIndexWithoutPrefix } = this.getDecimalCharIndexes(inputValue); + + if (this.isNumeralChar(deleteChar)) { + const decimalLength = this.getDecimalLength(inputValue); + + if (this._group.test(deleteChar)) { + this._group.lastIndex = 0; + newValueStr = inputValue.slice(0, selectionStart) + inputValue.slice(selectionStart + 2); + } else if (this._decimal.test(deleteChar)) { + this._decimal.lastIndex = 0; + + if (decimalLength) { + this.input?.nativeElement.setSelectionRange(selectionStart + 1, selectionStart + 1); + } else { + newValueStr = inputValue.slice(0, selectionStart) + inputValue.slice(selectionStart + 1); + } + } else if (decimalCharIndex > 0 && selectionStart > decimalCharIndex) { + const insertedText = this.isDecimalMode() && (this.minFractionDigits || 0) < decimalLength ? '' : '0'; + newValueStr = inputValue.slice(0, selectionStart) + insertedText + inputValue.slice(selectionStart + 1); + } else if (decimalCharIndexWithoutPrefix === 1) { + newValueStr = inputValue.slice(0, selectionStart) + '0' + inputValue.slice(selectionStart + 1); + newValueStr = (this.parseValue(newValueStr) as number) > 0 ? newValueStr : ''; + } else { + newValueStr = inputValue.slice(0, selectionStart) + inputValue.slice(selectionStart + 1); + } + } + + this.updateValue(event, newValueStr as string, null, 'delete-back-single'); + } else { + newValueStr = this.deleteRange(inputValue, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, null, 'delete-range'); + } + break; + + case 'Home': + if (this.min) { + this.updateModel(event, this.min); + event.preventDefault(); + } + break; + + case 'End': + if (this.max) { + this.updateModel(event, this.max); + event.preventDefault(); + } + break; + + default: + break; + } + + this.onKeyDown.emit(event); + } + + onInputKeyPress(event: KeyboardEvent) { + if (this.readonly) { + return; + } + + let code = event.which || event.keyCode; + let char = String.fromCharCode(code); + let isDecimalSign = this.isDecimalSign(char); + const isMinusSign = this.isMinusSign(char); + + if (code != 13) { + event.preventDefault(); + } + if (!isDecimalSign && event.code === 'NumpadDecimal') { + isDecimalSign = true; + char = this._decimalChar; + code = char.charCodeAt(0); + } + + const newValue = this.parseValue(this.input.nativeElement.value + char); + const newValueStr = newValue != null ? newValue.toString() : ''; + if (this.maxlength && newValueStr.length > this.maxlength) { + return; + } + + if ((48 <= code && code <= 57) || isMinusSign || isDecimalSign) { + this.insert(event, char, { isDecimalSign, isMinusSign }); + } + } + + onPaste(event: ClipboardEvent) { + if (!this.disabled && !this.readonly) { + event.preventDefault(); + let data = (event.clipboardData || (this.document as any).defaultView['clipboardData']).getData('Text'); + if (data) { + if (this.maxlength) { + data = data.toString().substring(0, this.maxlength); + } + + let filteredData = this.parseValue(data); + if (filteredData != null) { + this.insert(event, filteredData.toString()); + } + } + } + } + + allowMinusSign() { + return this.min == null || this.min < 0; + } + + isMinusSign(char: string) { + if (this._minusSign.test(char) || char === '-') { + this._minusSign.lastIndex = 0; + return true; + } + + return false; + } + + isDecimalSign(char: string) { + if (this._decimal.test(char)) { + this._decimal.lastIndex = 0; + return true; + } + + return false; + } + + isDecimalMode() { + return this.mode === 'decimal'; + } + + getDecimalCharIndexes(val: string) { + let decimalCharIndex = val.search(this._decimal); + this._decimal.lastIndex = 0; + + const filteredVal = val + .replace(this._prefix as RegExp, '') + .trim() + .replace(/\s/g, '') + .replace(this._currency as RegExp, ''); + const decimalCharIndexWithoutPrefix = filteredVal.search(this._decimal); + this._decimal.lastIndex = 0; + + return { decimalCharIndex, decimalCharIndexWithoutPrefix }; + } + + getCharIndexes(val: string) { + const decimalCharIndex = val.search(this._decimal); + this._decimal.lastIndex = 0; + const minusCharIndex = val.search(this._minusSign); + this._minusSign.lastIndex = 0; + const suffixCharIndex = val.search(this._suffix as RegExp); + (this._suffix as RegExp).lastIndex = 0; + const currencyCharIndex = val.search(this._currency as RegExp); + (this._currency as RegExp).lastIndex = 0; + + return { decimalCharIndex, minusCharIndex, suffixCharIndex, currencyCharIndex }; + } + + insert(event: Event, text: string, sign = { isDecimalSign: false, isMinusSign: false }) { + const minusCharIndexOnText = text.search(this._minusSign); + this._minusSign.lastIndex = 0; + if (!this.allowMinusSign() && minusCharIndexOnText !== -1) { + return; + } + + let selectionStart = this.input?.nativeElement.selectionStart; + let selectionEnd = this.input?.nativeElement.selectionEnd; + let inputValue = this.input?.nativeElement.value.trim(); + const { decimalCharIndex, minusCharIndex, suffixCharIndex, currencyCharIndex } = this.getCharIndexes(inputValue); + let newValueStr; + + if (sign.isMinusSign) { + if (selectionStart === 0) { + newValueStr = inputValue; + if (minusCharIndex === -1 || selectionEnd !== 0) { + newValueStr = this.insertText(inputValue, text, 0, selectionEnd); + } + + this.updateValue(event, newValueStr, text, 'insert'); + } + } else if (sign.isDecimalSign) { + if (decimalCharIndex > 0 && selectionStart === decimalCharIndex) { + this.updateValue(event, inputValue, text, 'insert'); + } else if (decimalCharIndex > selectionStart && decimalCharIndex < selectionEnd) { + newValueStr = this.insertText(inputValue, text, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, text, 'insert'); + } else if (decimalCharIndex === -1 && this.maxFractionDigits) { + newValueStr = this.insertText(inputValue, text, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, text, 'insert'); + } + } else { + const maxFractionDigits = this.numberFormat.resolvedOptions().maximumFractionDigits; + const operation = selectionStart !== selectionEnd ? 'range-insert' : 'insert'; + + if (decimalCharIndex > 0 && selectionStart > decimalCharIndex) { + if (selectionStart + text.length - (decimalCharIndex + 1) <= maxFractionDigits) { + const charIndex = currencyCharIndex >= selectionStart ? currencyCharIndex - 1 : suffixCharIndex >= selectionStart ? suffixCharIndex : inputValue.length; + + newValueStr = inputValue.slice(0, selectionStart) + text + inputValue.slice(selectionStart + text.length, charIndex) + inputValue.slice(charIndex); + this.updateValue(event, newValueStr, text, operation); + } + } else { + newValueStr = this.insertText(inputValue, text, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, text, operation); + } + } + } + + insertText(value: string, text: string, start: number, end: number) { + let textSplit = text === '.' ? text : text.split('.'); + + if (textSplit.length === 2) { + const decimalCharIndex = value.slice(start, end).search(this._decimal); + this._decimal.lastIndex = 0; + return decimalCharIndex > 0 ? value.slice(0, start) + this.formatValue(text) + value.slice(end) : value || this.formatValue(text); + } else if (end - start === value.length) { + return this.formatValue(text); + } else if (start === 0) { + return text + value.slice(end); + } else if (end === value.length) { + return value.slice(0, start) + text; + } else { + return value.slice(0, start) + text + value.slice(end); + } + } + + deleteRange(value: string, start: number, end: number) { + let newValueStr; + + if (end - start === value.length) newValueStr = ''; + else if (start === 0) newValueStr = value.slice(end); + else if (end === value.length) newValueStr = value.slice(0, start); + else newValueStr = value.slice(0, start) + value.slice(end); + + return newValueStr; + } + + initCursor() { + let selectionStart = this.input?.nativeElement.selectionStart; + let inputValue = this.input?.nativeElement.value; + let valueLength = inputValue.length; + let index = null; + + // remove prefix + let prefixLength = (this.prefixChar || '').length; + inputValue = inputValue.replace(this._prefix, ''); + selectionStart = selectionStart - prefixLength; + + let char = inputValue.charAt(selectionStart); + if (this.isNumeralChar(char)) { + return selectionStart + prefixLength; + } + + //left + let i = selectionStart - 1; + while (i >= 0) { + char = inputValue.charAt(i); + if (this.isNumeralChar(char)) { + index = i + prefixLength; + break; + } else { + i--; + } + } + + if (index !== null) { + this.input?.nativeElement.setSelectionRange(index + 1, index + 1); + } else { + i = selectionStart; + while (i < valueLength) { + char = inputValue.charAt(i); + if (this.isNumeralChar(char)) { + index = i + prefixLength; + break; + } else { + i++; + } + } + + if (index !== null) { + this.input?.nativeElement.setSelectionRange(index, index); + } + } + + return index || 0; + } + + onInputClick() { + const currentValue = this.input?.nativeElement.value; + + if (!this.readonly && currentValue !== DomHandler.getSelection()) { + this.initCursor(); + } + } + + isNumeralChar(char: string) { + if (char.length === 1 && (this._numeral.test(char) || this._decimal.test(char) || this._group.test(char) || this._minusSign.test(char))) { + this.resetRegex(); + return true; + } + + return false; + } + + resetRegex() { + this._numeral.lastIndex = 0; + this._decimal.lastIndex = 0; + this._group.lastIndex = 0; + this._minusSign.lastIndex = 0; + } + + updateValue(event: Event, valueStr: Nullable, insertedValueStr: Nullable, operation: Nullable) { + let currentValue = this.input?.nativeElement.value; + let newValue = null; + + if (valueStr != null) { + newValue = this.parseValue(valueStr); + newValue = !newValue && !this.allowEmpty ? 0 : newValue; + this.updateInput(newValue, insertedValueStr, operation, valueStr); + + this.handleOnInput(event, currentValue, newValue); + } + } + + handleOnInput(event: Event, currentValue: string, newValue: any) { + if (this.isValueChanged(currentValue, newValue)) { + (this.input as ElementRef).nativeElement.value = this.formatValue(newValue); + this.input?.nativeElement.setAttribute('aria-valuenow', newValue); + this.updateModel(event, newValue); + this.onInput.emit({ originalEvent: event, value: newValue, formattedValue: currentValue }); + } + } + + isValueChanged(currentValue: string, newValue: string) { + if (newValue === null && currentValue !== null) { + return true; + } + + if (newValue != null) { + let parsedCurrentValue = typeof currentValue === 'string' ? this.parseValue(currentValue) : currentValue; + return newValue !== parsedCurrentValue; + } + + return false; + } + + validateValue(value: number | string) { + if (value === '-' || value == null) { + return null; + } + + if (this.min != null && (value as number) < this.min) { + return this.min; + } + + if (this.max != null && (value as number) > this.max) { + return this.max; + } + + return value; + } + + updateInput(value: any, insertedValueStr: Nullable, operation: Nullable, valueStr: Nullable) { + insertedValueStr = insertedValueStr || ''; + + let inputValue = this.input?.nativeElement.value; + let newValue = this.formatValue(value); + let currentLength = inputValue.length; + + if (newValue !== valueStr) { + newValue = this.concatValues(newValue, valueStr as string); + } + + if (currentLength === 0) { + this.input.nativeElement.value = newValue; + this.input.nativeElement.setSelectionRange(0, 0); + const index = this.initCursor(); + const selectionEnd = index + insertedValueStr.length; + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else { + let selectionStart = this.input.nativeElement.selectionStart; + let selectionEnd = this.input.nativeElement.selectionEnd; + + if (this.maxlength && newValue.length > this.maxlength) { + newValue = newValue.slice(0, this.maxlength); + selectionStart = Math.min(selectionStart, this.maxlength); + selectionEnd = Math.min(selectionEnd, this.maxlength); + } + + if (this.maxlength && this.maxlength < newValue.length) { + return; + } + + this.input.nativeElement.value = newValue; + let newLength = newValue.length; + + if (operation === 'range-insert') { + const startValue = this.parseValue((inputValue || '').slice(0, selectionStart)); + const startValueStr = startValue !== null ? startValue.toString() : ''; + const startExpr = startValueStr.split('').join(`(${this.groupChar})?`); + const sRegex = new RegExp(startExpr, 'g'); + sRegex.test(newValue); + + const tExpr = insertedValueStr.split('').join(`(${this.groupChar})?`); + const tRegex = new RegExp(tExpr, 'g'); + tRegex.test(newValue.slice(sRegex.lastIndex)); + + selectionEnd = sRegex.lastIndex + tRegex.lastIndex; + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else if (newLength === currentLength) { + if (operation === 'insert' || operation === 'delete-back-single') this.input.nativeElement.setSelectionRange(selectionEnd + 1, selectionEnd + 1); + else if (operation === 'delete-single') this.input.nativeElement.setSelectionRange(selectionEnd - 1, selectionEnd - 1); + else if (operation === 'delete-range' || operation === 'spin') this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else if (operation === 'delete-back-single') { + let prevChar = inputValue.charAt(selectionEnd - 1); + let nextChar = inputValue.charAt(selectionEnd); + let diff = currentLength - newLength; + let isGroupChar = this._group.test(nextChar); + + if (isGroupChar && diff === 1) { + selectionEnd += 1; + } else if (!isGroupChar && this.isNumeralChar(prevChar)) { + selectionEnd += -1 * diff + 1; + } + + this._group.lastIndex = 0; + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else if (inputValue === '-' && operation === 'insert') { + this.input.nativeElement.setSelectionRange(0, 0); + const index = this.initCursor(); + const selectionEnd = index + insertedValueStr.length + 1; + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else { + selectionEnd = selectionEnd + (newLength - currentLength); + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } + } + + this.input.nativeElement.setAttribute('aria-valuenow', value); + } + + concatValues(val1: string, val2: string) { + if (val1 && val2) { + let decimalCharIndex = val2.search(this._decimal); + this._decimal.lastIndex = 0; + + if (this.suffixChar) { + return decimalCharIndex !== -1 ? val1 : val1.replace(this.suffixChar, '').split(this._decimal)[0] + val2.replace(this.suffixChar, '').slice(decimalCharIndex) + this.suffixChar; + } else { + return decimalCharIndex !== -1 ? val1.split(this._decimal)[0] + val2.slice(decimalCharIndex) : val1; + } + } + return val1; + } + + getDecimalLength(value: string) { + if (value) { + const valueSplit = value.split(this._decimal); + + if (valueSplit.length === 2) { + return valueSplit[1] + .replace(this._suffix as RegExp, '') + .trim() + .replace(/\s/g, '') + .replace(this._currency as RegExp, '').length; + } + } + + return 0; + } + + onInputFocus(event: Event) { + this.focused = true; + this.onFocus.emit(event); + } + + onInputBlur(event: Event) { + this.focused = false; + + let newValue = this.validateValue(this.parseValue(this.input.nativeElement.value));//?.toString(); + this.input.nativeElement.value = this.formatValue(newValue); + this.input.nativeElement.setAttribute('aria-valuenow', newValue?.toString()); + this.updateModel(event, newValue); + this.onBlur.emit(event); + } + + formattedValue() { + const val = !this.value && !this.allowEmpty ? 0 : this.value; + return this.formatValue(val); + } + + updateModel(event: Event, value: any) { + const isBlurUpdateOnMode = this.ngControl?.control?.updateOn === 'blur'; + + if (this.value !== value) { + this.value = value; + + if (!(isBlurUpdateOnMode && this.focused)) { + this.onModelChange(value); + } + } else if (isBlurUpdateOnMode) { + this.onModelChange(value); + } + this.onModelTouched(); + } + + writeValue(value: any): void { + this.value = value; + this.cd.markForCheck(); + } + + registerOnChange(fn: Function): void { + this.onModelChange = fn; + } + + registerOnTouched(fn: Function): void { + this.onModelTouched = fn; + } + + setDisabledState(val: boolean): void { + this.disabled = val; + this.cd.markForCheck(); + } + + get filled() { + return this.value != null && this.value.toString().length > 0; + } + + clearTimer() { + if (this.timer) { + clearInterval(this.timer); + } + } +} + +@NgModule({ + imports: [CommonModule, InputTextModule, ButtonModule, TimesIcon, AngleUpIcon, AngleDownIcon], + exports: [InputNumberComponent, SharedModule], + declarations: [InputNumberComponent] +}) +export class InputNumberModule {} \ No newline at end of file diff --git a/bizmatch/src/app/pages/details/details.component.html b/bizmatch/src/app/pages/details/details.component.html index cba4a52..5f715c8 100644 --- a/bizmatch/src/app/pages/details/details.component.html +++ b/bizmatch/src/app/pages/details/details.component.html @@ -85,7 +85,7 @@
{{listing.category}}
} --> - @if (listing && (listing.listingsCategory==='investment')){ + @if (listing && (listing.listingsCategory==='commercialProperty')){
  • Located in
    {{selectOptions.getState(listing.state)}}
    diff --git a/bizmatch/src/app/pages/home/home.component.html b/bizmatch/src/app/pages/home/home.component.html index a860f15..af00958 100644 --- a/bizmatch/src/app/pages/home/home.component.html +++ b/bizmatch/src/app/pages/home/home.component.html @@ -5,7 +5,7 @@
  • -
  • +
  • diff --git a/bizmatch/src/app/pages/listings/listings.component.html b/bizmatch/src/app/pages/listings/listings.component.html index b276f35..ac324de 100644 --- a/bizmatch/src/app/pages/listings/listings.component.html +++ b/bizmatch/src/app/pages/listings/listings.component.html @@ -29,24 +29,24 @@ offLabel="Real Estate included"> } - @if (listingCategory==='investment'){ + @if (listingCategory==='commercialProperty'){
    -
    } - @if (listingCategory==='professionals_brokers'){ -
    + + + +
    @@ -70,24 +70,15 @@ {{selectOptions.getBusiness(listing.type)}} } - @if (listing.listingsCategory==='professionals_brokers'){ -
    - - - - {{selectOptions.getCategory(listing.category)}} -
    - } - @if (listing.listingsCategory==='investment'){ -
    + @if (listing.listingsCategory==='commercialProperty'){ + }
    {{listing.title}}
    @if (listing.listingsCategory==='business'){ @@ -97,17 +88,11 @@

    Location: {{selectOptions.getState(listing.state)}}

    Established: {{listing.established}}

    } - @if (listing.listingsCategory==='professionals_brokers'){ - -

    Location: {{selectOptions.getState(listing.state)}}

    + @if (listing.listingsCategory==='commercialProperty'){ + }
    diff --git a/bizmatch/src/app/pages/listings/listings.component.ts b/bizmatch/src/app/pages/listings/listings.component.ts index b192b27..0b3773c 100644 --- a/bizmatch/src/app/pages/listings/listings.component.ts +++ b/bizmatch/src/app/pages/listings/listings.component.ts @@ -40,7 +40,7 @@ export class ListingsComponent { first: number = 0; rows: number = 12; totalRecords:number = 0; - public listingCategory: 'business' | 'professionals_brokers' | 'investment' | undefined; + public listingCategory: 'business' | 'commercialProperty' | undefined; constructor(public selectOptions: SelectOptionsService, private listingsService:ListingsService,private activatedRoute: ActivatedRoute, private router:Router, private cdRef:ChangeDetectorRef) { this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler); diff --git a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.html b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.html index 71a4ac9..1c45bde 100644 --- a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.html +++ b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.html @@ -47,7 +47,7 @@
    - +
    - +
    - +
    - +
    - +
    @@ -130,7 +131,7 @@
    - +
    @@ -149,9 +150,9 @@ }
    @if (mode==='create'){ - + } @else { - + }
    diff --git a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.ts b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.ts index cbdd022..12988c1 100644 --- a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.ts @@ -20,26 +20,27 @@ import { TableModule } from 'primeng/table'; import { createGenericObject } from '../../../utils/utils'; import { ListingsService } from '../../../services/listings.service'; import { lastValueFrom } from 'rxjs'; -import { InputNumberModule } from 'primeng/inputnumber'; + import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe'; import { UserService } from '../../../services/user.service'; import { SharedModule } from '../../../shared/shared/shared.module'; import { MessageService } from 'primeng/api'; import { AutoCompleteCompleteEvent, BusinessListing, ListingType, User } from '../../../../../../common-models/src/main.model'; import { GeoResult, GeoService } from '../../../services/geo.service'; +import { InputNumberComponent, InputNumberModule } from '../../../components/inputnumber/inputnumber.component'; @Component({ selector: 'create-listing', standalone: true, - imports: [SharedModule,ArrayToStringPipe], + imports: [SharedModule,ArrayToStringPipe, InputNumberModule], providers:[MessageService], templateUrl: './edit-listing.component.html', styleUrl: './edit-listing.component.scss' }) export class EditListingComponent { - listingCategory:'Business'|'Professionals/Brokers Directory'|'Investment Property'; + listingCategory:'Business'|'Commercial Property'; category:string; location:string; mode:'edit'|'create'; @@ -47,6 +48,7 @@ export class EditListingComponent { listing:ListingType = createGenericObject(); private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; user:User; + value:12; constructor(public selectOptions:SelectOptionsService, private router: Router, private activatedRoute: ActivatedRoute, @@ -66,6 +68,7 @@ export class EditListingComponent { async ngOnInit(){ if (this.mode==='edit'){ this.listing=await lastValueFrom(this.listingsService.getListingById(this.id)); + this.listing.price=123456 } else { this.listing=createGenericObject(); this.listing.userId=this.user.id @@ -76,19 +79,19 @@ export class EditListingComponent { // const lines = value.split('\n'); // (this.listing).summary = lines.filter(l=>l.trim().length>0); // } - async update(id:string){ - await this.listingsService.update(this.listing,this.listing.id); - this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing has been updated', life: 3000 }); - } - async create(){ - await this.listingsService.create(this.listing); - this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing has been created', life: 3000 }); + // async update(id:string){ + // await this.listingsService.update(this.listing,this.listing.id); + // this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing has been updated', life: 3000 }); + // } + async save(){ + await this.listingsService.save(this.listing); + this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 }); } suggestions: string[] | undefined; async search(event: AutoCompleteCompleteEvent) { - const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query,this.listing.state))//[...Array(5).keys()].map(item => event.query + '-' + item); + const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query,this.listing.state)) this.suggestions = result.map(r=>r.city).slice(0,5); } } diff --git a/bizmatch/src/app/services/listings.service.ts b/bizmatch/src/app/services/listings.service.ts index abcebcb..6bb50e8 100644 --- a/bizmatch/src/app/services/listings.service.ts +++ b/bizmatch/src/app/services/listings.service.ts @@ -12,22 +12,22 @@ export class ListingsService { constructor(private http: HttpClient) { } getAllListings():Observable{ - return this.http.get(`${this.apiBaseUrl}/bizmatch/listings`); + return this.http.get(`${this.apiBaseUrl}/bizmatch/business-listings`); } async getListings(criteria:ListingCriteria):Promise{ - const result = await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/listings/search`,criteria)); + const result = await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/business-listings/search`,criteria)); return result; } getListingById(id:string):Observable{ - return this.http.get(`${this.apiBaseUrl}/bizmatch/listings/${id}`); + return this.http.get(`${this.apiBaseUrl}/bizmatch/business-listings/${id}`); } - async update(listing:any,id:string){ - await lastValueFrom(this.http.put(`${this.apiBaseUrl}/bizmatch/listings/${id}`,listing)); - } - async create(listing:any){ - await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/listings`,listing)); + // async update(listing:any,id:string){ + // await lastValueFrom(this.http.put(`${this.apiBaseUrl}/bizmatch/listings/${id}`,listing)); + // } + async save(listing:any){ + await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/business-listings`,listing)); } async deleteListing(id:string){ - await lastValueFrom(this.http.delete(`${this.apiBaseUrl}/bizmatch/listings/${id}`)); + await lastValueFrom(this.http.delete(`${this.apiBaseUrl}/bizmatch/business-listings/${id}`)); } } diff --git a/bizmatch/src/app/shared/shared/shared.module.ts b/bizmatch/src/app/shared/shared/shared.module.ts index 756d8ae..cac5dbc 100644 --- a/bizmatch/src/app/shared/shared/shared.module.ts +++ b/bizmatch/src/app/shared/shared/shared.module.ts @@ -22,6 +22,7 @@ import { ConfirmPopupModule } from 'primeng/confirmpopup'; import { ToastModule } from 'primeng/toast'; import { AutoCompleteModule } from 'primeng/autocomplete'; import { InputSwitchModule } from 'primeng/inputswitch'; + @NgModule({ declarations: [], imports: [ diff --git a/bizmatch/src/main.ts b/bizmatch/src/main.ts index 87ae19a..d7b1b04 100644 --- a/bizmatch/src/main.ts +++ b/bizmatch/src/main.ts @@ -4,5 +4,6 @@ import { AppComponent } from './app/app.component'; import { provideHttpClient } from '@angular/common/http'; import { provideAnimations } from '@angular/platform-browser/animations'; + bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err)); diff --git a/common-models/src/main.model.ts b/common-models/src/main.model.ts index 7925cb3..217fb50 100644 --- a/common-models/src/main.model.ts +++ b/common-models/src/main.model.ts @@ -46,32 +46,31 @@ export interface BusinessListing extends Listing { brokerLicencing?: string; internals?: string; } -export interface ProfessionalsBrokersListing extends Listing { - listingsCategory: 'professionals_brokers'; //enum - summary: string; - address?: string; - email?: string; - website?: string; - category?: 'Professionals' | 'Broker'; -} -export interface InvestmentsListing extends Listing { - listingsCategory: 'investment'; //enum +// export interface ProfessionalsBrokersListing extends Listing { +// listingsCategory: 'professionals_brokers'; //enum +// summary: string; +// address?: string; +// email?: string; +// website?: string; +// category?: 'Professionals' | 'Broker'; +// } +export interface CommercialPropertyListing extends Listing { + listingsCategory: 'commercialProperty'; //enum email?: string; website?: string; phoneNumber?: string; } export type ListingType = | BusinessListing - | ProfessionalsBrokersListing - | InvestmentsListing; + | CommercialPropertyListing; export interface ListingCriteria { type:string, - location:string, + state:string, minPrice:string, maxPrice:string, realEstateChecked:boolean, - listingsCategory:'business'|'professionals_brokers'|'investment', + listingsCategory:'business'|'professionals_brokers'|'commercialProperty', category:'professional|broker' } export interface UserBase {