validatedCity, mask for input, confirmatonService, Version Info,

This commit is contained in:
Andreas Knuth 2024-08-07 13:11:26 +02:00
parent 8698aa3e66
commit 3795a5a30c
44 changed files with 360313 additions and 119860 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,14 @@ import { join } from 'path';
import pkg from 'pg';
import { rimraf } from 'rimraf';
import sharp from 'sharp';
import { Geo } from 'src/models/server.model.js';
import winston from 'winston';
import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js';
import { createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js';
import { SelectOptionsService } from '../select-options/select-options.service.js';
import { toDrizzleUser } from '../utils.js';
import { convertUserToDrizzleUser } from '../utils.js';
import * as schema from './schema.js';
const typesOfBusiness: Array<KeyValueStyle> = [
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
{ name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
@ -47,7 +49,7 @@ await db.delete(schema.businesses);
await db.delete(schema.users);
let filePath = `./src/assets/geo.json`;
const rawData = readFileSync(filePath, 'utf8');
const geos = JSON.parse(rawData);
const geos = JSON.parse(rawData) as Geo;
const sso = new SelectOptionsService();
//Broker
@ -94,28 +96,22 @@ for (let index = 0; index < usersData.length; index++) {
user.companyName = userData.companyName;
user.companyOverview = userData.companyOverview;
user.companyWebsite = userData.companyWebsite;
user.companyLocation = userData.companyLocation;
const [city, state] = user.companyLocation.split('-').map(e => e.trim());
const [city, state] = userData.companyLocation.split('-').map(e => e.trim());
user.companyLocation.city = city;
user.companyLocation.state = state;
const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city);
user.latitude = cityGeo.latitude;
user.longitude = cityGeo.longitude;
user.companyLocation.latitude = cityGeo.latitude;
user.companyLocation.longitude = cityGeo.longitude;
user.offeredServices = userData.offeredServices;
user.gender = userData.gender;
user.customerType = 'professional';
user.customerSubType = 'broker';
user.created = new Date();
user.updated = new Date();
// const createUserProfile = (user: User): UserProfile => {
// const { id, created, updated, hasCompanyLogo, hasProfile, ...userProfile } = user;
// return userProfile;
// };
// const userProfile = createUserProfile(user);
// logger.info(`${index} - ${JSON.stringify(userProfile)}`);
// const embedding = await createEmbedding(JSON.stringify(userProfile));
//sleep(200);
const u = await db
.insert(schema.users)
.values(toDrizzleUser(user))
.values(convertUserToDrizzleUser(user))
.returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname });
generatedUserData.push(u[0]);
i++;

View File

@ -0,0 +1,3 @@
ALTER TABLE "users" ADD COLUMN "city" varchar(255);--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "state" char(2);--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN IF EXISTS "companyLocation";

View File

@ -0,0 +1,589 @@
{
"id": "1d2566aa-6103-4520-a648-c0abdda08189",
"prevId": "146c197a-0ef7-4b10-84cd-352b88aba859",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "listingsCategory",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"imageName": {
"name": "imageName",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_email_users_email_fk": {
"name": "businesses_email_users_email_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"serialId": {
"name": "serialId",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "listingsCategory",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_email_users_email_fk": {
"name": "commercials_email_users_email_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gender": {
"name": "gender",
"type": "gender",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerType": {
"name": "customerType",
"type": "customerType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerSubType": {
"name": "customerSubType",
"type": "customerSubType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
}
}
},
"enums": {
"public.customerSubType": {
"name": "customerSubType",
"schema": "public",
"values": [
"broker",
"cpa",
"attorney",
"titleCompany",
"surveyor",
"appraiser"
]
},
"public.customerType": {
"name": "customerType",
"schema": "public",
"values": [
"buyer",
"professional"
]
},
"public.gender": {
"name": "gender",
"schema": "public",
"values": [
"male",
"female"
]
},
"public.listingsCategory": {
"name": "listingsCategory",
"schema": "public",
"values": [
"commercialProperty",
"business"
]
}
},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -22,6 +22,13 @@
"when": 1722853523826,
"tag": "0002_chemical_gambit",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1722964164111,
"tag": "0003_robust_blockbuster",
"breakpoints": true
}
]
}

View File

@ -7,7 +7,7 @@ export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', '
export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']);
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
id: uuid('id').primaryKey().defaultRandom().notNull(),
firstname: varchar('firstname', { length: 255 }).notNull(),
lastname: varchar('lastname', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
@ -16,7 +16,8 @@ export const users = pgTable('users', {
companyName: varchar('companyName', { length: 255 }),
companyOverview: text('companyOverview'),
companyWebsite: varchar('companyWebsite', { length: 255 }),
companyLocation: varchar('companyLocation', { length: 255 }),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
offeredServices: text('offeredServices'),
areasServed: jsonb('areasServed').$type<AreasServed[]>(),
hasProfile: boolean('hasProfile'),
@ -33,7 +34,7 @@ export const users = pgTable('users', {
});
export const businesses = pgTable('businesses', {
id: uuid('id').primaryKey().defaultRandom(),
id: uuid('id').primaryKey().defaultRandom().notNull(),
email: varchar('email', { length: 255 }).references(() => users.email),
type: varchar('type', { length: 255 }),
title: varchar('title', { length: 255 }),
@ -69,7 +70,7 @@ export const businesses = pgTable('businesses', {
});
export const commercials = pgTable('commercials', {
id: uuid('id').primaryKey().defaultRandom(),
id: uuid('id').primaryKey().defaultRandom().notNull(),
serialId: serial('serialId'),
email: varchar('email', { length: 255 }).references(() => users.email),
type: varchar('type', { length: 255 }),

View File

@ -53,16 +53,18 @@ export class GeoService {
result.push({
id: city.id,
city: city.name,
state: state.name,
state_code: state.state_code,
state: state.state_code,
//state_code: state.state_code,
latitude: city.latitude,
longitude: city.longitude,
});
}
});
});
return state ? result.filter(e => e.state_code.toLowerCase() === state.toLowerCase()) : result;
return state ? result.filter(e => e.state.toLowerCase() === state.toLowerCase()) : result;
}
findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<{ id: string; name: string; type: 'city' | 'state'; state_code: string }> {
const results: Array<{ id: string; name: string; type: 'city' | 'state'; state_code: string }> = [];
findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> {
const results: Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> = [];
const lowercasePrefix = prefix.toLowerCase();
@ -74,7 +76,7 @@ export class GeoService {
id: state.id.toString(),
name: state.name,
type: 'state',
state_code: state.state_code,
state: state.state_code,
});
}
@ -85,7 +87,7 @@ export class GeoService {
id: city.id.toString(),
name: city.name,
type: 'city',
state_code: state.state_code,
state: state.state_code,
});
}
}

View File

@ -29,7 +29,7 @@ export class BusinessListingService {
}
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
whereConditions.push(sql`${getDistanceQuery(businesses, parseFloat(cityGeo.latitude), parseFloat(cityGeo.longitude))} <= ${criteria.radius}`);
whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
}
if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(businesses.type, criteria.types));

View File

@ -28,7 +28,7 @@ export class CommercialPropertyService {
}
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
whereConditions.push(sql`${getDistanceQuery(commercials, parseFloat(cityGeo.latitude), parseFloat(cityGeo.longitude))} <= ${criteria.radius}`);
whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
}
if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(schema.commercials.type, criteria.types));

View File

@ -109,8 +109,27 @@ export const LicensedInSchema = z.object({
registerNo: z.string().nonempty('Registration number is required'),
state: z.string().nonempty('State is required'),
});
const phoneRegex = /^\+1 \(\d{3}\) \d{3}-\d{4}$/;
export const GeoSchema = z.object({
city: z.string(),
state: z.string(),
latitude: z.number().refine(
value => {
return value >= -90 && value <= 90;
},
{
message: 'Latitude muss zwischen -90 und 90 liegen',
},
),
longitude: z.number().refine(
value => {
return value >= -180 && value <= 180;
},
{
message: 'Longitude muss zwischen -180 und 180 liegen',
},
),
});
const phoneRegex = /^\(\d{3}\)\s\d{3}-\d{4}$/;
export const UserSchema = z
.object({
@ -123,7 +142,7 @@ export const UserSchema = z
companyName: z.string().optional().nullable(),
companyOverview: z.string().optional().nullable(),
companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
companyLocation: z.string().optional().nullable(),
companyLocation: GeoSchema,
offeredServices: z.string().optional().nullable(),
areasServed: z.array(AreasServedSchema).optional().nullable(),
hasProfile: z.boolean().optional().nullable(),
@ -134,8 +153,6 @@ export const UserSchema = z
customerSubType: CustomerSubTypeEnum.optional().nullable(),
created: z.date().optional().nullable(),
updated: z.date().optional().nullable(),
latitude: z.number().optional().nullable(),
longitude: z.number().optional().nullable(),
})
.superRefine((data, ctx) => {
if (data.customerType === 'professional') {
@ -150,7 +167,7 @@ export const UserSchema = z
if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Phone number is required and must be in US format (+1 (XXX) XXX-XXXX) for professional customers',
message: 'Phone number is required and must be in US format (XXX) XXX-XXXX for professional customers',
path: ['phoneNumber'],
});
}
@ -236,8 +253,22 @@ export const BusinessListingSchema = z.object({
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(),
latitude: z.number().refine(
value => {
return value >= -90 && value <= 90;
},
{
message: 'Latitude muss zwischen -90 und 90 liegen',
},
),
longitude: z.number().refine(
value => {
return value >= -180 && value <= 180;
},
{
message: 'Longitude muss zwischen -180 und 180 liegen',
},
),
});
export type BusinessListing = z.infer<typeof BusinessListingSchema>;
@ -268,8 +299,22 @@ export const CommercialPropertyListingSchema = z
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(),
latitude: z.number().refine(
value => {
return value >= -90 && value <= 90;
},
{
message: 'Latitude muss zwischen -90 und 90 liegen',
},
),
longitude: z.number().refine(
value => {
return value >= -180 && value <= 180;
},
{
message: 'Longitude muss zwischen -180 und 180 liegen',
},
),
})
.strict();

View File

@ -228,13 +228,15 @@ export interface GeoResult {
id: number;
city: string;
state: string;
state_code: string;
// state_code: string;
latitude: number;
longitude: number;
}
export interface CityAndStateResult {
id: number;
name: string;
type: string;
state_code: string;
state: string;
}
export interface CountyResult {
id: number;

View File

@ -18,8 +18,8 @@ export interface Geo {
nationality: string;
timezones: Timezone[];
translations: Translations;
latitude: string;
longitude: string;
latitude: number;
longitude: number;
emoji: string;
emojiU: string;
states: State[];
@ -28,16 +28,16 @@ export interface State {
id: number;
name: string;
state_code: string;
latitude: string;
longitude: string;
latitude: number;
longitude: number;
type: string;
cities: City[];
}
export interface City {
id: number;
name: string;
latitude: string;
longitude: string;
latitude: number;
longitude: number;
}
export interface Translations {
kr: string;

View File

@ -10,7 +10,7 @@ import { FileService } from '../file/file.service.js';
import { GeoService } from '../geo/geo.service.js';
import { User, UserSchema } from '../models/db.model.js';
import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
import { getDistanceQuery, toDrizzleUser } from '../utils.js';
import { convertDrizzleUserToUser, convertUserToDrizzleUser, getDistanceQuery } from '../utils.js';
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
@Injectable()
@ -26,11 +26,11 @@ export class UserService {
const whereConditions: SQL[] = [];
whereConditions.push(eq(schema.users.customerType, 'professional'));
if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(ilike(schema.users.companyLocation, `%${criteria.city}%`));
whereConditions.push(ilike(schema.users.city, `%${criteria.city}%`));
}
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
whereConditions.push(sql`${getDistanceQuery(schema.users, parseFloat(cityGeo.latitude), parseFloat(cityGeo.longitude))} <= ${criteria.radius}`);
whereConditions.push(sql`${getDistanceQuery(schema.users, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
}
if (criteria.types && criteria.types.length > 0) {
// whereConditions.push(inArray(schema.users.customerSubType, criteria.types));
@ -99,12 +99,13 @@ export class UserService {
.where(sql`email = ${email}`)) as User[];
if (users.length === 0) {
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname) };
return await this.saveUser(user);
const u = await this.saveUser(user);
return convertDrizzleUserToUser(u);
} else {
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user;
return convertDrizzleUserToUser(user);
}
}
async getUserById(id: string) {
@ -127,14 +128,13 @@ export class UserService {
user.created = new Date();
}
const validatedUser = UserSchema.parse(user);
const drizzleUser = toDrizzleUser(validatedUser);
const drizzleUser = convertUserToDrizzleUser(validatedUser);
if (user.id) {
const [updateUser] = await this.conn.update(schema.users).set(drizzleUser).where(eq(schema.users.id, user.id)).returning();
return updateUser as User;
} else {
const drizzleUser = toDrizzleUser(user);
const [newUser] = await this.conn.insert(schema.users).values(drizzleUser).returning();
return newUser as User;
return convertDrizzleUserToUser(newUser) as User;
}
} catch (error) {
if (error instanceof ZodError) {

View File

@ -1,10 +1,8 @@
import { sql } from 'drizzle-orm';
import { z } from 'zod';
import { businesses, commercials, users } from './drizzle/schema.js';
import { AreasServedSchema, CustomerSubTypeEnum, CustomerTypeEnum, GenderEnum, LicensedInSchema, User } from './models/db.model.js';
import { User } from './models/db.model.js';
export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
export function convertStringToNullUndefined(value) {
// Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung
const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase();
@ -31,32 +29,70 @@ export const getDistanceQuery = (schema: typeof businesses | typeof commercials
`;
};
export function toDrizzleUser(user: User): {
email: string;
firstname: string;
lastname: string;
phoneNumber?: string;
description?: string;
companyName?: string;
companyOverview?: string;
companyWebsite?: string;
companyLocation?: string;
offeredServices?: string;
areasServed?: (typeof AreasServedSchema._type)[];
hasProfile?: boolean;
hasCompanyLogo?: boolean;
licensedIn?: (typeof LicensedInSchema._type)[];
gender?: z.infer<typeof GenderEnum>;
customerType?: z.infer<typeof CustomerTypeEnum>;
customerSubType?: z.infer<typeof CustomerSubTypeEnum>;
latitude?: number;
longitude?: number;
} {
const { id, created, updated, ...drizzleUser } = user;
type DrizzleUser = typeof users.$inferSelect; //Partial<InferInsertModel<typeof users>>;
export function convertUserToDrizzleUser(user: Partial<User>): DrizzleUser {
const { companyLocation, ...restUser } = user;
// Ensure all required fields are present
if (!user.id || !user.email || !user.firstname || !user.lastname) {
throw new Error('Missing required fields: id, email, firstname, or lastname');
}
return {
...drizzleUser,
email: drizzleUser.email,
firstname: drizzleUser.firstname,
lastname: drizzleUser.lastname,
id: user.id,
email: user.email,
firstname: user.firstname,
lastname: user.lastname,
phoneNumber: user.phoneNumber || null,
description: user.description || null,
companyName: user.companyName || null,
companyOverview: user.companyOverview || null,
companyWebsite: user.companyWebsite || null,
city: companyLocation?.city || null,
state: companyLocation?.state || null,
offeredServices: user.offeredServices || null,
areasServed: user.areasServed || [],
hasProfile: user.hasProfile || false,
hasCompanyLogo: user.hasCompanyLogo || false,
licensedIn: user.licensedIn || [],
gender: user.gender || null,
customerType: user.customerType || null,
customerSubType: user.customerSubType || null,
created: user.created || new Date(),
updated: user.updated || new Date(),
latitude: companyLocation?.latitude || 0,
longitude: companyLocation?.longitude || 0,
};
}
export function convertDrizzleUserToUser(drizzleUser: Partial<DrizzleUser>): User {
const user = {
id: drizzleUser.id,
firstname: drizzleUser.firstname,
lastname: drizzleUser.lastname,
email: drizzleUser.email,
phoneNumber: drizzleUser.phoneNumber ?? null,
description: drizzleUser.description ?? null,
companyName: drizzleUser.companyName ?? null,
companyOverview: drizzleUser.companyOverview ?? null,
companyWebsite: drizzleUser.companyWebsite ?? null,
companyLocation: {
city: drizzleUser.city,
state: drizzleUser.state,
latitude: drizzleUser.latitude, // Latitude wird zugewiesen, auch wenn es nicht direkt benötigt wird
longitude: drizzleUser.longitude, // Longitude wird zugewiesen, auch wenn es nicht direkt benötigt wird
},
offeredServices: drizzleUser.offeredServices ?? null,
areasServed: drizzleUser.areasServed ?? null,
hasProfile: drizzleUser.hasProfile ?? null,
hasCompanyLogo: drizzleUser.hasCompanyLogo ?? null,
licensedIn: drizzleUser.licensedIn ?? null,
gender: drizzleUser.gender ?? null,
customerType: drizzleUser.customerType,
customerSubType: drizzleUser.customerSubType ?? null,
created: drizzleUser.created ?? null,
updated: drizzleUser.updated ?? null,
};
return user;
}

View File

@ -43,6 +43,7 @@
"memoize-one": "^6.0.0",
"ngx-currency": "^18.0.0",
"ngx-image-cropper": "^8.0.0",
"ngx-mask": "^18.0.0",
"ngx-quill": "^26.0.5",
"on-change": "^5.0.1",
"rxjs": "~7.8.1",

View File

@ -11,12 +11,45 @@
@if (loadingService.isLoading$ | async) {
<div class="spinner-overlay">
<div class="spinner-container">
@let loadingText = (loadingService.loadingText$ | async);
<!-- @if(loadingService.loadingText$ | async){ -->
@let loadingText = (loadingService.loadingText$ | async); @if(loadingText){
<div class="spinner-text">{{ loadingText }}</div>
<!-- } -->
}
<div role="status">
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<!-- <span class="sr-only">Loading ...</span> -->
</div>
</div>
</div>
}
<!-- <div *ngIf="loadingService.isLoading$ | async" class="spinner-overlay">
<div class="spinner-container">
<ng-container *ngIf="loadingService.loadingText$ | async as loadingText">
<div *ngIf="loadingText" class="spinner-text">{{ loadingText }}</div>
</ng-container>
<div role="status">
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
</div> -->
<app-message-container></app-message-container>
<app-search-modal></app-search-modal>
<app-confirmation></app-confirmation>

View File

@ -1,25 +1,25 @@
.progress-spinner {
position: fixed;
z-index: 999;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
}
// .progress-spinner {
// position: fixed;
// z-index: 999;
// top: 0;
// left: 0;
// bottom: 0;
// right: 0;
// display: flex;
// flex-direction: column;
// align-items: center;
// }
.progress-spinner:before {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
}
// .progress-spinner:before {
// content: '';
// display: block;
// position: fixed;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// background-color: rgba(0, 0, 0, 0.3);
// }
.spinner-text {
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
font-size: 20px; /* Schriftgröße nach Bedarf anpassen */

View File

@ -5,6 +5,8 @@ import { KeycloakService } from 'keycloak-angular';
import { filter } from 'rxjs/operators';
import build from '../build';
import { ConfirmationComponent } from './components/confirmation/confirmation.component';
import { ConfirmationService } from './components/confirmation/confirmation.service';
import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component';
import { MessageContainerComponent } from './components/message/message-container.component';
@ -15,7 +17,7 @@ import { UserService } from './services/user.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent],
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent],
providers: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
@ -25,7 +27,14 @@ export class AppComponent {
title = 'bizmatch';
actualRoute = '';
public constructor(public loadingService: LoadingService, private router: Router, private activatedRoute: ActivatedRoute, private keycloakService: KeycloakService, private userService: UserService) {
public constructor(
public loadingService: LoadingService,
private router: Router,
private activatedRoute: ActivatedRoute,
private keycloakService: KeycloakService,
private userService: UserService,
private confirmationService: ConfirmationService,
) {
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
let currentRoute = this.activatedRoute.root;
while (currentRoute.children[0] !== undefined) {
@ -49,13 +58,6 @@ export class AppComponent {
}
showVersionDialog() {
// this.confirmationService.confirm({
// target: event.target as EventTarget,
// message: `App Version: ${this.build.timestamp}`,
// header: 'Version Info',
// icon: 'pi pi-info-circle',
// accept: () => {},
// reject: () => {},
// });
this.confirmationService.showConfirmation({ message: `App Version: ${this.build.timestamp}`, buttons: 'none' });
}
}

View File

@ -24,7 +24,9 @@ import { ConfirmationService } from './confirmation.service';
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmationService.message$ | async }}</h3>
@let confirmation = (confirmationService.confirmation$ | async);
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation.message }}</h3>
@if(confirmation.buttons==='both'){
<button
(click)="confirmationService.accept()"
type="button"
@ -39,6 +41,7 @@ import { ConfirmationService } from './confirmation.service';
>
No, cancel
</button>
}
</div>
</div>
</div>

View File

@ -1,19 +1,25 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface Confirmation {
message: string;
buttons?: 'both' | 'none';
button_accept_label?: string;
button_reject_label?: string;
}
@Injectable({
providedIn: 'root',
})
export class ConfirmationService {
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
private messageSubject = new BehaviorSubject<string>('');
private confirmationSubject = new BehaviorSubject<Confirmation>({ message: '' });
private resolvePromise!: (value: boolean) => void;
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
message$: Observable<string> = this.messageSubject.asObservable();
confirmation$: Observable<Confirmation> = this.confirmationSubject.asObservable();
showConfirmation(message: string): Promise<boolean> {
this.messageSubject.next(message);
showConfirmation(confirmation: Confirmation): Promise<boolean> {
confirmation.buttons = confirmation.buttons ? confirmation.buttons : 'both';
this.confirmationSubject.next(confirmation);
this.modalVisibleSubject.next(true);
return new Promise<boolean>(resolve => {
this.resolvePromise = resolve;

View File

@ -1,7 +1,7 @@
import { CdkDrag, CdkDragEnd, CdkDragMove, DragDropModule, DragRef, moveItemInArray } from '@angular/cdk/drag-drop';
import { _getShadowRoot } from '@angular/cdk/platform';
import { CommonModule } from '@angular/common';
import { Component, ElementRef, input, output, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, Input, input, output, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { CommercialPropertyListing } from '../../../../../bizmatch-server/src/models/db.model';
import { environment } from '../../../environments/environment';
@Component({
@ -14,12 +14,11 @@ import { environment } from '../../../environments/environment';
export class DragDropMixedComponent {
@ViewChild('_container') _container!: ElementRef<HTMLDivElement>;
@ViewChildren(CdkDrag) _drags!: QueryList<CdkDrag>;
@Input() ts: number;
listing = input<CommercialPropertyListing>();
imageOrderChanged = output<string[]>();
imageToDelete = output<string>();
env = environment;
ts = new Date().getTime();
items: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];
private _cachedItems: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];
@ -34,12 +33,15 @@ export class DragDropMixedComponent {
};
private _containerStyle: CSSStyleDeclaration | null = null;
public isAnimationActive = false;
constructor(private cdr: ChangeDetectorRef) {}
ngOnChanges() {
this.items = this.listing()?.imageOrder;
this._cachedItems = this.items.slice();
}
ngAfterViewInit() {
// Führen Sie einen zusätzlichen Change Detection-Zyklus durch
this.cdr.detectChanges();
}
getImageUrl(image: string): string {
return `${this.env.imageBaseUrl}/pictures/property/${this.listing().imagePath}/${this.listing().serialId}/${image}?_ts=${this.ts}`;
}

View File

@ -48,23 +48,15 @@ export class ImageCropAndUploadComponent {
this.imageChangedEvent = null;
this.croppedImage = null;
this.showModal = false;
this.fileInput.nativeElement.value = '';
this.uploadFinished.emit({ success: false, type: this.uploadParams.type });
}
uploadImage() {
async uploadImage() {
if (this.croppedImage) {
this.imageService.uploadImage(this.croppedImage, this.uploadParams.type, this.uploadParams.imagePath, this.uploadParams.serialId).subscribe(
response => {
console.log('Upload successful', response);
this.closeModal();
this.uploadFinished.emit({ success: true, type: this.uploadParams.type });
},
error => {
console.error('Upload failed', error);
this.closeModal();
this.uploadFinished.emit({ success: false, type: this.uploadParams.type });
},
);
await this.imageService.uploadImage(this.croppedImage, this.uploadParams.type, this.uploadParams.imagePath, this.uploadParams.serialId);
this.closeModal();
this.uploadFinished.emit({ success: true, type: this.uploadParams.type });
}
}

View File

@ -39,7 +39,7 @@
(ngModelChange)="setCity($event)"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
<ng-option [value]="city">{{ city.city }} - {{ city.state }}</ng-option>
}
</ng-select>
</div>

View File

@ -98,7 +98,7 @@ export class SearchModalComponent {
setCity(city) {
if (city) {
this.criteria.city = city.city;
this.criteria.state = city.state_code;
this.criteria.state = city.state;
} else {
this.criteria.city = null;
this.criteria.radius = null;

View File

@ -0,0 +1,29 @@
<div>
<label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit"
>{{ label }} @if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage"></app-tooltip>
}
</label>
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$"
ngModel="{{ value?.city }} {{ value ? '-' : '' }} {{ value?.state }}"
(ngModelChange)="onInputChange($event)"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.city }} - {{ city.state }}</ng-option>
}
</ng-select>
</div>

View File

@ -0,0 +1,9 @@
:host ::ng-deep .ng-select.custom .ng-select-container {
// --tw-bg-opacity: 1;
// background-color: rgb(249 250 251 / var(--tw-bg-opacity));
height: 42px;
border-radius: 0.5rem;
.ng-value-container .ng-input {
top: 10px;
}
}

View File

@ -0,0 +1,68 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
import { GeoResult } from '../../../../../bizmatch-server/src/models/main.model';
import { City } from '../../../../../bizmatch-server/src/models/server.model';
import { GeoService } from '../../services/geo.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-city',
standalone: true,
imports: [CommonModule, FormsModule, NgSelectModule, TooltipComponent],
templateUrl: './validated-city.component.html',
styleUrl: './validated-city.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedCityComponent),
multi: true,
},
],
})
export class ValidatedCityComponent extends BaseInputComponent {
@Input() items;
cities$: Observable<GeoResult[]>;
cityInput$ = new Subject<string>();
countyInput$ = new Subject<string>();
cityLoading = false;
constructor(validationMessagesService: ValidationMessagesService, private geoService: GeoService, public selectOptions: SelectOptionsService) {
super(validationMessagesService);
}
override ngOnInit() {
super.ngOnInit();
this.loadCities();
}
onInputChange(event: City): void {
this.value = event; //{ ...event, longitude: parseFloat(event.longitude), latitude: parseFloat(event.latitude) };
this.onChange(this.value);
}
private loadCities() {
this.cities$ = concat(
of([]), // default items
this.cityInput$.pipe(
distinctUntilChanged(),
tap(() => (this.cityLoading = true)),
switchMap(term =>
this.geoService.findCitiesStartingWith(term).pipe(
catchError(() => of([])), // empty list on error
// map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names
tap(() => (this.cityLoading = false)),
),
),
),
);
}
trackByFn(item: GeoResult) {
return item.id;
}
compareFn = (item, selected) => {
return item.id === selected.id;
};
}

View File

@ -19,5 +19,7 @@
(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"
[mask]="mask"
[dropSpecialCharacters]="false"
/>
</div>

View File

@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@ -9,17 +10,19 @@ import { ValidationMessagesService } from '../validation-messages.service';
selector: 'app-validated-input',
templateUrl: './validated-input.component.html',
standalone: true,
imports: [CommonModule, FormsModule, TooltipComponent],
imports: [CommonModule, FormsModule, TooltipComponent, NgxMaskDirective, NgxMaskPipe],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedInputComponent),
multi: true,
},
provideNgxMask(),
],
})
export class ValidatedInputComponent extends BaseInputComponent {
@Input() kind: 'text' | 'number' | 'email' | 'tel' = 'text';
@Input() mask: string;
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}

View File

@ -84,7 +84,7 @@ export class DetailsBusinessListingComponent {
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation.state };
}
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
this.listingUser = await this.userService.getByMail(this.listing.email);

View File

@ -86,7 +86,7 @@ export class DetailsCommercialPropertyListingComponent {
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation.state };
}
this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
this.listingUser = await this.userService.getByMail(this.listing.email);

View File

@ -114,7 +114,7 @@
groupBy="type"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.name }} - {{ city.state_code }}</ng-option>
<ng-option [value]="city">{{ city.name }} - {{ city.state }}</ng-option>
}
</ng-select>
</div>

View File

@ -154,10 +154,10 @@ export class HomeComponent {
setCityOrState(cityOrState: CityAndStateResult) {
if (cityOrState) {
if (cityOrState.type === 'state') {
this.criteria.state = cityOrState.state_code;
this.criteria.state = cityOrState.state;
} else {
this.criteria.city = cityOrState.name;
this.criteria.state = cityOrState.state_code;
this.criteria.state = cityOrState.state;
this.criteria.searchType = 'radius';
this.criteria.radius = 20;
}

View File

@ -115,9 +115,10 @@
<label for="companyLocation" class="block text-sm font-medium text-gray-700">Company Location</label>
<input type="text" id="companyLocation" name="companyLocation" [(ngModel)]="user.companyLocation" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
</div> -->
<app-validated-input label="Your Phone Number" name="phoneNumber" [(ngModel)]="user.phoneNumber"></app-validated-input>
<app-validated-input label="Your Phone Number" name="phoneNumber" [(ngModel)]="user.phoneNumber" mask="(000) 000-0000"></app-validated-input>
<app-validated-input label="Company Website" name="companyWebsite" [(ngModel)]="user.companyWebsite"></app-validated-input>
<app-validated-input label="Company Location" name="companyLocation" [(ngModel)]="user.companyLocation"></app-validated-input>
<!-- <app-validated-input label="Company Location" name="companyLocation" [(ngModel)]="user.companyLocation"></app-validated-input> -->
<app-validated-city label="Company Location" name="companyLocation" [(ngModel)]="user.companyLocation"></app-validated-city>
</div>
<!-- <div>

View File

@ -18,6 +18,7 @@ import { ImageCropAndUploadComponent, UploadReponse } from '../../../components/
import { MessageComponent } from '../../../components/message/message.component';
import { MessageService } from '../../../components/message/message.service';
import { TooltipComponent } from '../../../components/tooltip/tooltip.component';
import { ValidatedCityComponent } from '../../../components/validated-city/validated-city.component';
import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
import { ValidatedQuillComponent } from '../../../components/validated-quill/validated-quill.component';
import { ValidatedSelectComponent } from '../../../components/validated-select/validated-select.component';
@ -47,6 +48,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
ValidatedInputComponent,
ValidatedSelectComponent,
ValidatedQuillComponent,
ValidatedCityComponent,
TooltipComponent,
],
providers: [TitleCasePipe],
@ -167,7 +169,7 @@ export class AccountComponent {
async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => `${r.city} - ${r.state_code}`).slice(0, 5);
this.suggestions = result.map(r => `${r.city} - ${r.state}`).slice(0, 5);
}
addLicence() {
this.user.licensedIn.push({ registerNo: '', state: '' });
@ -204,7 +206,7 @@ export class AccountComponent {
}
}
async deleteConfirm(type: 'profile' | 'logo') {
const confirmed = await this.confirmationService.showConfirmation(`Do you want to delete your ${type === 'logo' ? 'Logo' : 'Profile'} image`);
const confirmed = await this.confirmationService.showConfirmation({ message: `Do you want to delete your ${type === 'logo' ? 'Logo' : 'Profile'} image` });
if (confirmed) {
if (type === 'profile') {
this.user.hasProfile = false;

View File

@ -127,7 +127,7 @@
</div>
</div>
} -->
<app-drag-drop-mixed [listing]="listing" (imageOrderChanged)="imageOrderChanged($event)" (imageToDelete)="deleteConfirm($event)"></app-drag-drop-mixed>
<app-drag-drop-mixed [listing]="listing" [ts]="ts" (imageOrderChanged)="imageOrderChanged($event)" (imageToDelete)="deleteConfirm($event)"></app-drag-drop-mixed>
<!-- </div> -->
</div>
@if (mode!=='create'){

View File

@ -102,7 +102,6 @@ export class EditCommercialPropertyListingComponent {
showModal = false;
imageChangedEvent: any = '';
croppedImage: Blob | null = null;
constructor(
public selectOptions: SelectOptionsService,
private router: Router,
@ -197,28 +196,24 @@ export class EditCommercialPropertyListingComponent {
this.showModal = false;
}
uploadImage() {
async uploadImage() {
if (this.croppedImage) {
this.imageService.uploadImage(this.croppedImage, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe(
async () => {
this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
this.closeModal();
},
error => {
console.error('Upload failed', error);
},
);
await this.imageService.uploadImage(this.croppedImage, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId);
this.ts = new Date().getTime();
this.closeModal();
this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
}
}
async deleteConfirm(imageName: string) {
const confirmed = await this.confirmationService.showConfirmation('Are you sure you want to delete this image?');
const confirmed = await this.confirmationService.showConfirmation({ message: 'Are you sure you want to delete this image?' });
if (confirmed) {
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
await this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName);
await this.listingsService.save(this.listing, 'commercialProperty');
this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
this.messageService.addMessage({ severity: 'success', text: 'Image has been deleted', duration: 3000 });
this.ts = new Date().getTime();
} else {
console.log('deny');
}

View File

@ -39,7 +39,7 @@ export class EmailUsComponent {
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation.state };
}
}
ngOnDestroy() {

View File

@ -54,7 +54,7 @@ export class MyListingComponent {
}
async confirm(listing: ListingType) {
const confirmed = await this.confirmationService.showConfirmation(`Are you sure you want to delete this listing?`);
const confirmed = await this.confirmationService.showConfirmation({ message: `Are you sure you want to delete this listing?` });
if (confirmed) {
// this.messageService.showMessage('Listing has been deleted');
this.deleteListing(listing);

View File

@ -12,7 +12,7 @@ export class ImageService {
constructor(private http: HttpClient) {}
uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string, serialId?: number) {
async uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string, serialId?: number) {
let uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`;
if (type === 'uploadPropertyPicture') {
uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}/${serialId}`;
@ -20,9 +20,10 @@ export class ImageService {
const formData = new FormData();
formData.append('file', imageBlob, 'image.png');
return this.http.post(uploadUrl, formData, {
observe: 'events',
});
// return this.http.post(uploadUrl, formData, {
// observe: 'events',
// });
return await lastValueFrom(this.http.post(uploadUrl, formData));
}
async deleteListingImage(imagePath: string, serial: number, name?: string) {

View File

@ -1,13 +1,15 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, debounceTime, distinctUntilChanged, map, shareReplay } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class LoadingService {
public loading$ = new BehaviorSubject<string[]>([]);
private loading$ = new BehaviorSubject<string[]>([]);
private loadingTextSubject = new BehaviorSubject<string | null>(null);
private excludedUrls: string[] = ['/findTotal', '/geo']; // Liste der URLs, für die kein Ladeindikator angezeigt werden soll
loadingText$: Observable<string | null> = this.loadingTextSubject.asObservable();
public isLoading$ = this.loading$.asObservable().pipe(
@ -17,13 +19,15 @@ export class LoadingService {
shareReplay(1),
);
public startLoading(type: string, request?: string): void {
if (!this.loading$.value.includes(type)) {
this.loading$.next(this.loading$.value.concat(type));
if (type === 'uploadImage' || request?.includes('uploadImage') || request?.includes('uploadPropertyPicture') || request?.includes('uploadProfile') || request?.includes('uploadCompanyLogo')) {
this.loadingTextSubject.next("Please wait - we're processing your image...");
} else {
this.loadingTextSubject.next(null);
public startLoading(type: string, url?: string): void {
if (this.shouldShowLoading(url)) {
if (!this.loading$.value.includes(type)) {
this.loading$.next(this.loading$.value.concat(type));
if (this.isImageUpload(type, url)) {
this.loadingTextSubject.next("Please wait - we're processing your image...");
} else {
this.loadingTextSubject.next(null);
}
}
}
}
@ -34,4 +38,13 @@ export class LoadingService {
this.loadingTextSubject.next(null);
}
}
private shouldShowLoading(url?: string): boolean {
if (!url) return true;
return !this.excludedUrls.some(excludedUrl => url.includes(excludedUrl));
}
private isImageUpload(type: string, url?: string): boolean {
return type === 'uploadImage' || url?.includes('uploadImage') || url?.includes('uploadPropertyPicture') || url?.includes('uploadProfile') || url?.includes('uploadCompanyLogo');
}
}