diff --git a/bizmatch-server/src/listings/business-listing.service.ts b/bizmatch-server/src/listings/business-listing.service.ts index ab17c26..5812211 100644 --- a/bizmatch-server/src/listings/business-listing.service.ts +++ b/bizmatch-server/src/listings/business-listing.service.ts @@ -1,13 +1,14 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; +import { ZodError } from 'zod'; import * as schema from '../drizzle/schema.js'; import { businesses, PG_CONNECTION } from '../drizzle/schema.js'; import { FileService } from '../file/file.service.js'; import { GeoService } from '../geo/geo.service.js'; -import { BusinessListing, CommercialPropertyListing } from '../models/db.model'; +import { BusinessListing, BusinessListingSchema } from '../models/db.model.js'; import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js'; import { getDistanceQuery } from '../utils.js'; @@ -169,17 +170,41 @@ export class BusinessListingService { } // #### CREATE ######################################## async createListing(data: BusinessListing): Promise { - data.created = new Date(); - data.updated = new Date(); - const [createdListing] = await this.conn.insert(businesses).values(data).returning(); - return createdListing as BusinessListing; + try { + data.created = new Date(); + data.updated = new Date(); + const validatedBusinessListing = BusinessListingSchema.parse(data); + const [createdListing] = await this.conn.insert(businesses).values(validatedBusinessListing).returning(); + return createdListing as BusinessListing; + } catch (error) { + if (error instanceof ZodError) { + const formattedErrors = error.errors.map(err => ({ + field: err.path.join('.'), + message: err.message, + })); + throw new BadRequestException(formattedErrors); + } + throw error; + } } // #### UPDATE Business ######################################## - async updateBusinessListing(id: string, data: BusinessListing): Promise { - data.updated = new Date(); - data.created = new Date(data.created); - const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning(); - return updateListing as BusinessListing | CommercialPropertyListing; + async updateBusinessListing(id: string, data: BusinessListing): Promise { + try { + data.updated = new Date(); + data.created = new Date(data.created); + const validatedBusinessListing = BusinessListingSchema.parse(data); + const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning(); + return updateListing as BusinessListing; + } catch (error) { + if (error instanceof ZodError) { + const formattedErrors = error.errors.map(err => ({ + field: err.path.join('.'), + message: err.message, + })); + throw new BadRequestException(formattedErrors); + } + throw error; + } } // #### DELETE ######################################## async deleteListing(id: string): Promise { diff --git a/bizmatch-server/src/listings/commercial-property.service.ts b/bizmatch-server/src/listings/commercial-property.service.ts index 5bcbbbb..1f0ce26 100644 --- a/bizmatch-server/src/listings/commercial-property.service.ts +++ b/bizmatch-server/src/listings/commercial-property.service.ts @@ -1,13 +1,14 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; +import { ZodError } from 'zod'; import * as schema from '../drizzle/schema.js'; import { commercials, PG_CONNECTION } from '../drizzle/schema.js'; import { FileService } from '../file/file.service.js'; import { GeoService } from '../geo/geo.service.js'; -import { CommercialPropertyListing } from '../models/db.model'; +import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model.js'; import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js'; import { getDistanceQuery } from '../utils.js'; @@ -123,23 +124,47 @@ export class CommercialPropertyService { } // #### CREATE ######################################## async createListing(data: CommercialPropertyListing): Promise { - data.created = new Date(); - data.updated = new Date(); - const [createdListing] = await this.conn.insert(commercials).values(data).returning(); - return createdListing as CommercialPropertyListing; + try { + data.created = new Date(); + data.updated = new Date(); + const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data); + const [createdListing] = await this.conn.insert(commercials).values(validatedCommercialPropertyListing).returning(); + return createdListing as CommercialPropertyListing; + } catch (error) { + if (error instanceof ZodError) { + const formattedErrors = error.errors.map(err => ({ + field: err.path.join('.'), + message: err.message, + })); + throw new BadRequestException(formattedErrors); + } + throw error; + } } // #### UPDATE CommercialProps ######################################## async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise { - data.updated = new Date(); - data.created = new Date(data.created); - const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId)); - let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x))); - if (difference.length > 0) { - this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`); - data.imageOrder = imageOrder; + try { + data.updated = new Date(); + data.created = new Date(data.created); + const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data); + const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId)); + let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x))); + if (difference.length > 0) { + this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`); + data.imageOrder = imageOrder; + } + const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning(); + return updateListing as CommercialPropertyListing; + } catch (error) { + if (error instanceof ZodError) { + const formattedErrors = error.errors.map(err => ({ + field: err.path.join('.'), + message: err.message, + })); + throw new BadRequestException(formattedErrors); + } + throw error; } - const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning(); - return updateListing as CommercialPropertyListing; } // ############################################################## // Images for commercial Properties diff --git a/bizmatch-server/src/mail/mail.service.ts b/bizmatch-server/src/mail/mail.service.ts index 839868a..0f13d3e 100644 --- a/bizmatch-server/src/mail/mail.service.ts +++ b/bizmatch-server/src/mail/mail.service.ts @@ -1,7 +1,9 @@ import { MailerService } from '@nestjs-modules/mailer'; -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import path, { join } from 'path'; import { fileURLToPath } from 'url'; +import { ZodError } from 'zod'; +import { SenderSchema } from '../models/db.model.js'; import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model.js'; import { UserService } from '../user/user.service.js'; const __filename = fileURLToPath(import.meta.url); @@ -15,9 +17,19 @@ export class MailService { ) {} async sendInquiry(mailInfo: MailInfo): Promise { - //const user = await this.authService.getUser(mailInfo.userId) as KeycloakUser; + try { + const validatedSender = SenderSchema.parse(mailInfo.sender); + } catch (error) { + if (error instanceof ZodError) { + const formattedErrors = error.errors.map(err => ({ + field: err.path.join('.'), + message: err.message, + })); + throw new BadRequestException(formattedErrors); + } + throw error; + } const user = await this.userService.getUserByMail(mailInfo.email); - console.log(JSON.stringify(user)); if (isEmpty(mailInfo.sender.name)) { return { fields: [{ fieldname: 'name', message: 'Required' }] }; } @@ -42,8 +54,17 @@ export class MailService { }); } async sendRequest(mailInfo: MailInfo): Promise { - if (isEmpty(mailInfo.sender.name)) { - return { fields: [{ fieldname: 'name', message: 'Required' }] }; + try { + const validatedSender = SenderSchema.parse(mailInfo.sender); + } catch (error) { + if (error instanceof ZodError) { + const formattedErrors = error.errors.map(err => ({ + field: err.path.join('.'), + message: err.message, + })); + throw new BadRequestException(formattedErrors); + } + throw error; } await this.mailerService.sendMail({ to: 'support@bizmatch.net', diff --git a/bizmatch-server/src/models/db.model.ts b/bizmatch-server/src/models/db.model.ts index def6d21..2f31c69 100644 --- a/bizmatch-server/src/models/db.model.ts +++ b/bizmatch-server/src/models/db.model.ts @@ -1,28 +1,5 @@ -// export interface User { -// id?: string; -// firstname: string; -// lastname: string; -// email: string; -// phoneNumber?: string; -// description?: string; -// companyName?: string; -// companyOverview?: string; -// companyWebsite?: string; -// companyLocation?: string; -// offeredServices?: string; -// areasServed?: AreasServed[]; -// hasProfile?: boolean; -// hasCompanyLogo?: boolean; -// licensedIn?: LicensedIn[]; -// gender?: 'male' | 'female'; -// customerType?: 'buyer' | 'professional'; -// customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser'; -// created?: Date; -// updated?: Date; - import { z } from 'zod'; -// } export interface UserData { id?: string; firstname: string; @@ -49,45 +26,80 @@ export type Gender = 'male' | 'female'; export type CustomerType = 'buyer' | 'professional'; export type CustomerSubType = 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser'; export type ListingsCategory = 'commercialProperty' | 'business'; -// export interface User { -// id: string; // UUID as a string -// firstname: string; -// lastname: string; -// email: string; -// phoneNumber?: string; -// description?: string; -// companyName?: string; -// companyOverview?: string; -// companyWebsite?: string; -// companyLocation?: string; -// offeredServices?: string; -// areasServed?: AreasServed[]; -// hasProfile?: boolean; -// hasCompanyLogo?: boolean; -// licensedIn?: LicensedIn[]; -// gender?: Gender; -// customerType?: CustomerType; -// customerSubType?: CustomerSubType; -// created?: Date; -// updated?: Date; -// latitude?: number; -// longitude?: number; -// } -// export interface AreasServed { -// county: string; -// state: string; -// } -// export interface LicensedIn { -// registerNo: string; -// state: string; -// } -// -------------------------------- -// -// -------------------------------- + export const GenderEnum = z.enum(['male', 'female']); export const CustomerTypeEnum = z.enum(['buyer', 'professional']); export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']); export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']); +const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']); +const TypeEnum = z.enum([ + 'automotive', + 'industrialServices', + 'foodAndRestaurant', + 'realEstate', + 'retail', + 'oilfield', + 'service', + 'advertising', + 'agriculture', + 'franchise', + 'professional', + 'manufacturing', + 'uncategorized', +]); + +const USStates = z.enum([ + 'AL', + 'AK', + 'AZ', + 'AR', + 'CA', + 'CO', + 'CT', + 'DE', + 'FL', + 'GA', + 'HI', + 'ID', + 'IL', + 'IN', + 'IA', + 'KS', + 'KY', + 'LA', + 'ME', + 'MD', + 'MA', + 'MI', + 'MN', + 'MS', + 'MO', + 'MT', + 'NE', + 'NV', + 'NH', + 'NJ', + 'NM', + 'NY', + 'NC', + 'ND', + 'OH', + 'OK', + 'OR', + 'PA', + 'RI', + 'SC', + 'SD', + 'TN', + 'TX', + 'UT', + 'VT', + 'VA', + 'WA', + 'WV', + 'WI', + 'WY', +]); export const AreasServedSchema = z.object({ county: z.string().nonempty('County is required'), state: z.string().nonempty('State is required'), @@ -98,56 +110,6 @@ export const LicensedInSchema = z.object({ state: z.string().nonempty('State is required'), }); -// export const UserSchema = z -// .object({ -// id: z.string().uuid('Invalid ID format. Must be a valid UUID').optional(), -// firstname: z.string().min(2, 'First name must be at least 2 characters long'), -// lastname: z.string().min(2, 'Last name must be at least 2 characters long'), -// email: z.string().email('Invalid email address'), -// phoneNumber: z.string().optional().nullable(), -// description: z.string().min(10, 'Description must be at least 10 characters long').optional().nullable(), -// companyName: z.string().optional().nullable(), -// companyOverview: z.string().min(10, 'Company overview must be at least 10 characters long').optional().nullable(), -// companyWebsite: z.string().url('Invalid company website URL').optional().nullable(), -// companyLocation: z.string().optional().nullable(), // Additional validation for US locations could be implemented here -// offeredServices: z.string().min(10, 'Offered services must be at least 10 characters long').optional().nullable(), -// areasServed: z.array(AreasServedSchema).optional().nullable(), -// hasProfile: z.boolean().optional().nullable(), -// hasCompanyLogo: z.boolean().optional().nullable(), -// licensedIn: z.array(LicensedInSchema).optional().nullable(), -// gender: GenderEnum.optional().nullable(), -// customerType: CustomerTypeEnum.optional().nullable(), -// customerSubType: CustomerSubTypeEnum.optional().nullable(), -// created: z.date().optional().nullable(), -// updated: z.date().optional().nullable(), -// latitude: z.number().optional().nullable(), -// longitude: z.number().optional().nullable(), -// }) -// .refine( -// data => { -// if (data.customerType === 'professional') { -// return !!data.customerSubType && !!data.phoneNumber && !!data.companyOverview && !!data.description && !!data.offeredServices && !!data.companyLocation && data.areasServed && data.areasServed.length > 0; -// } -// return true; -// }, -// { -// message: 'For professional customers, additional fields are required: customer subtype, phone number, company overview, description, offered services, company location, and at least one area served', -// path: ['customerType'], -// }, -// ) -// .refine( -// data => { -// if (data.customerType === 'professional') { -// return /\(\d{3}\) \d{3}-\d{4}$/.test(data.phoneNumber || ''); -// } -// return true; -// }, -// { -// message: 'Phone number must be in US format: +1 (XXX) XXX-XXXX', -// path: ['phoneNumber'], -// }, -// ); - const phoneRegex = /^\+1 \(\d{3}\) \d{3}-\d{4}$/; export const UserSchema = z @@ -238,64 +200,90 @@ export const UserSchema = z export type AreasServed = z.infer; export type LicensedIn = z.infer; export type User = z.infer; -export interface BusinessListing { - id: string; // UUID as a string - email: string; // References users.email - type?: string; - title?: string; - description?: string; - city?: string; - state?: string; // 2-character state code - zipCode?: number; - county?: string; - price?: number; // double precision - favoritesForUser?: string[]; // Array of strings - draft?: boolean; - listingsCategory?: ListingsCategory; - realEstateIncluded?: boolean; - leasedLocation?: boolean; - franchiseResale?: boolean; - salesRevenue?: number; // double precision - cashFlow?: number; // double precision - supportAndTraining?: string; - employees?: number; - established?: number; - internalListingNumber?: number; - reasonForSale?: string; - brokerLicencing?: string; - internals?: string; - imageName?: string; - created?: Date; - updated?: Date; - visits?: number; - lastVisit?: Date; - latitude?: number; // double precision - longitude?: number; // double precision -} -export interface CommercialPropertyListing { - id: string; // UUID as a string - serialId: number; // Serial ID - email: string; // References users.email - type?: string; - title?: string; - description?: string; - city?: string; - state?: string; // 2-character state code - price?: number; // double precision - favoritesForUser?: string[]; // Array of strings - listingsCategory?: ListingsCategory; - hideImage?: boolean; - draft?: boolean; - zipCode?: number; - county?: string; - imageOrder?: string[]; // Array of strings - imagePath?: string; - created?: Date; - updated?: Date; - visits?: number; - lastVisit?: Date; - latitude?: number; // double precision - longitude?: number; // double precision - // embedding?: number[]; // Uncomment if needed for vector embedding -} +export const BusinessListingSchema = z.object({ + id: z.string().uuid().optional(), + email: z.string().email(), + type: z.string().refine(val => TypeEnum.safeParse(val).success, { + message: 'Invalid type. Must be one of: ' + TypeEnum.options.join(', '), + }), + title: z.string().min(10), + description: z.string().min(10), + city: z.string(), + state: z.string().refine(val => USStates.safeParse(val).success, { + message: 'Invalid state. Must be a valid 2-letter US state code.', + }), + zipCode: z.number().int().positive().optional().nullable(), + county: z.string().optional().nullable(), + price: z.number().positive().max(100000000), + favoritesForUser: z.array(z.string()), + draft: z.boolean(), + listingsCategory: ListingsCategoryEnum, + realEstateIncluded: z.boolean().optional().nullable(), + leasedLocation: z.boolean().optional().nullable(), + franchiseResale: z.boolean().optional().nullable(), + salesRevenue: z.number().positive().max(100000000), + cashFlow: z.number().positive().max(100000000), + supportAndTraining: z.string().min(5), + employees: z.number().int().positive().max(100000).optional().nullable(), + established: z.number().int().min(1800).max(2030).optional().nullable(), + internalListingNumber: z.number().int().positive().optional().nullable(), + reasonForSale: z.string().min(5).optional().nullable(), + brokerLicencing: z.string().min(5).optional().nullable(), + internals: z.string().min(5).optional().nullable(), + imageName: z.string().optional().nullable(), + created: z.date(), + updated: z.date(), + visits: z.number().int().positive().optional().nullable(), + lastVisit: z.date().optional().nullable(), + latitude: z.number().optional().nullable(), + longitude: z.number().optional().nullable(), +}); +export type BusinessListing = z.infer; + +export const CommercialPropertyListingSchema = z + .object({ + id: z.string().uuid().optional(), + serialId: z.number().int().positive().optional(), + email: z.string().email(), + //type: PropertyTypeEnum.optional(), + type: z.string().refine(val => PropertyTypeEnum.safeParse(val).success, { + message: 'Invalid type. Must be one of: ' + PropertyTypeEnum.options.join(', '), + }), + title: z.string().min(10), + description: z.string().min(10), + city: z.string(), // You might want to add a custom validation for valid US cities + state: z.string().refine(val => USStates.safeParse(val).success, { + message: 'Invalid state. Must be a valid 2-letter US state code.', + }), // You might want to add a custom validation for valid US states + price: z.number().positive().max(100000000), + favoritesForUser: z.array(z.string()), + listingsCategory: ListingsCategoryEnum, + draft: z.boolean(), + zipCode: z.number().int().positive().nullable().optional(), // You might want to add a custom validation for valid US zip codes + county: z.string().nullable().optional(), // You might want to add a custom validation for valid US counties + imageOrder: z.array(z.string()), + imagePath: z.string().nullable().optional(), + created: z.date(), + updated: z.date(), + visits: z.number().int().positive().nullable().optional(), + lastVisit: z.date().nullable().optional(), + latitude: z.number().nullable().optional(), + longitude: z.number().nullable().optional(), + }) + .strict(); + +export type CommercialPropertyListing = z.infer; + +export const SenderSchema = z.object({ + name: z.string().min(6, { message: 'Name must be at least 6 characters long' }), + email: z.string().email({ message: 'Invalid email address' }), + phoneNumber: z.string().regex(/^(\+1|1)?[-.\s]?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/, { + message: 'Invalid US phone number format', + }), + state: z.string().refine(val => USStates.safeParse(val).success, { + message: 'Invalid state. Must be a valid 2-letter US state code.', + }), + comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }), +}); +export type Sender = z.infer; diff --git a/bizmatch-server/src/models/main.model.ts b/bizmatch-server/src/models/main.model.ts index 3c8a659..3586f35 100644 --- a/bizmatch-server/src/models/main.model.ts +++ b/bizmatch-server/src/models/main.model.ts @@ -1,4 +1,4 @@ -import { BusinessListing, CommercialPropertyListing, User } from './db.model'; +import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js'; export interface StatesResult { state: string; @@ -199,13 +199,13 @@ export interface MailInfo { url: string; listing?: BusinessListing; } -export interface Sender { - name?: string; - email?: string; - phoneNumber?: string; - state?: string; - comments?: string; -} +// export interface Sender { +// name?: string; +// email?: string; +// phoneNumber?: string; +// state?: string; +// comments?: string; +// } export interface ImageProperty { id: string; code: string; @@ -309,20 +309,19 @@ export function createDefaultCommercialPropertyListing(): CommercialPropertyList return { id: undefined, serialId: undefined, - email: '', + email: null, type: null, - title: '', - description: '', - city: '', - state: '', + title: null, + description: null, + city: null, + state: null, price: null, favoritesForUser: [], - hideImage: false, draft: false, zipCode: null, - county: '', + county: null, imageOrder: [], - imagePath: '', + imagePath: null, created: null, updated: null, visits: null, @@ -335,12 +334,12 @@ export function createDefaultCommercialPropertyListing(): CommercialPropertyList export function createDefaultBusinessListing(): BusinessListing { return { id: undefined, - email: '', + email: null, type: null, - title: '', - description: '', - city: '', - state: '', + title: null, + description: null, + city: null, + state: null, price: null, favoritesForUser: [], draft: false, @@ -349,13 +348,13 @@ export function createDefaultBusinessListing(): BusinessListing { franchiseResale: false, salesRevenue: null, cashFlow: null, - supportAndTraining: '', + supportAndTraining: null, employees: null, established: null, internalListingNumber: null, - reasonForSale: '', - brokerLicencing: '', - internals: '', + reasonForSale: null, + brokerLicencing: null, + internals: null, created: null, updated: null, visits: null, diff --git a/bizmatch-server/src/user/user.service.ts b/bizmatch-server/src/user/user.service.ts index a13a485..6466b60 100644 --- a/bizmatch-server/src/user/user.service.ts +++ b/bizmatch-server/src/user/user.service.ts @@ -21,18 +21,7 @@ export class UserService { private fileService: FileService, private geoService: GeoService, ) {} - // private getConditions(criteria: UserListingCriteria): any[] { - // const conditions = []; - // if (criteria.states?.length > 0) { - // criteria.states.forEach(state => { - // conditions.push(sql`${schema.users.areasServed} @> ${JSON.stringify([{ state: state }])}`); - // }); - // } - // if (criteria.firstname || criteria.lastname) { - // conditions.push(or(ilike(schema.users.firstname, `%${criteria.lastname}%`), ilike(schema.users.lastname, `%${criteria.lastname}%`))); - // } - // return conditions; - // } + private getWhereConditions(criteria: UserListingCriteria): SQL[] { const whereConditions: SQL[] = []; @@ -161,25 +150,6 @@ export class UserService { throw error; } } - // async findUser(criteria: UserListingCriteria) { - // const start = criteria.start ? criteria.start : 0; - // const length = criteria.length ? criteria.length : 12; - // const conditions = this.getConditions(criteria); - // const [data, total] = await Promise.all([ - // this.conn - // .select() - // .from(schema.users) - // .where(and(...conditions)) - // .offset(start) - // .limit(length), - // this.conn - // .select({ count: sql`count(*)` }) - // .from(schema.users) - // .where(and(...conditions)) - // .then(result => Number(result[0].count)), - // ]); - // return { total, data }; - // } async getStates(): Promise { const query = sql`SELECT jsonb_array_elements(${schema.users.areasServed}) ->> 'state' AS state, COUNT(DISTINCT ${schema.users.id}) AS count FROM ${schema.users} GROUP BY state ORDER BY count DESC`; const result = await this.conn.execute(query); diff --git a/bizmatch/src/app/components/validated-input/validated-input.component.html b/bizmatch/src/app/components/validated-input/validated-input.component.html index 417116c..dbbe515 100644 --- a/bizmatch/src/app/components/validated-input/validated-input.component.html +++ b/bizmatch/src/app/components/validated-input/validated-input.component.html @@ -15,7 +15,7 @@ [type]="kind" [id]="name" [ngModel]="value" - (input)="onInputChange($event)" + (ngModelChange)="onInputChange($event)" (blur)="onTouched()" [attr.name]="name" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" diff --git a/bizmatch/src/app/components/validated-input/validated-input.component.ts b/bizmatch/src/app/components/validated-input/validated-input.component.ts index 6b06946..8e35caa 100644 --- a/bizmatch/src/app/components/validated-input/validated-input.component.ts +++ b/bizmatch/src/app/components/validated-input/validated-input.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; +import { Component, forwardRef, Input } from '@angular/core'; import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { BaseInputComponent } from '../base-input/base-input.component'; import { TooltipComponent } from '../tooltip/tooltip.component'; @@ -19,7 +19,6 @@ import { ValidationMessagesService } from '../validation-messages.service'; ], }) export class ValidatedInputComponent extends BaseInputComponent { - @Output() valueChange = new EventEmitter(); @Input() kind: 'text' | 'number' | 'email' | 'tel' = 'text'; constructor(validationMessagesService: ValidationMessagesService) { super(validationMessagesService); diff --git a/bizmatch/src/app/components/validated-ng-select/validated-ng-select.component.html b/bizmatch/src/app/components/validated-ng-select/validated-ng-select.component.html index c90a92d..653ce88 100644 --- a/bizmatch/src/app/components/validated-ng-select/validated-ng-select.component.html +++ b/bizmatch/src/app/components/validated-ng-select/validated-ng-select.component.html @@ -1,6 +1,6 @@