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

View File

@ -114,4 +114,4 @@
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
} }
} }

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 pkg from 'pg';
import { rimraf } from 'rimraf'; import { rimraf } from 'rimraf';
import sharp from 'sharp'; import sharp from 'sharp';
import { Geo } from 'src/models/server.model.js';
import winston from 'winston'; import winston from 'winston';
import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js'; import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js';
import { createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js'; import { createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js';
import { SelectOptionsService } from '../select-options/select-options.service.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'; import * as schema from './schema.js';
const typesOfBusiness: Array<KeyValueStyle> = [ const typesOfBusiness: Array<KeyValueStyle> = [
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' }, { 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' }, { 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); await db.delete(schema.users);
let filePath = `./src/assets/geo.json`; let filePath = `./src/assets/geo.json`;
const rawData = readFileSync(filePath, 'utf8'); const rawData = readFileSync(filePath, 'utf8');
const geos = JSON.parse(rawData); const geos = JSON.parse(rawData) as Geo;
const sso = new SelectOptionsService(); const sso = new SelectOptionsService();
//Broker //Broker
@ -94,28 +96,22 @@ for (let index = 0; index < usersData.length; index++) {
user.companyName = userData.companyName; user.companyName = userData.companyName;
user.companyOverview = userData.companyOverview; user.companyOverview = userData.companyOverview;
user.companyWebsite = userData.companyWebsite; user.companyWebsite = userData.companyWebsite;
user.companyLocation = userData.companyLocation; const [city, state] = userData.companyLocation.split('-').map(e => e.trim());
const [city, state] = user.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); const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city);
user.latitude = cityGeo.latitude; user.companyLocation.latitude = cityGeo.latitude;
user.longitude = cityGeo.longitude; user.companyLocation.longitude = cityGeo.longitude;
user.offeredServices = userData.offeredServices; user.offeredServices = userData.offeredServices;
user.gender = userData.gender; user.gender = userData.gender;
user.customerType = 'professional'; user.customerType = 'professional';
user.customerSubType = 'broker'; user.customerSubType = 'broker';
user.created = new Date(); user.created = new Date();
user.updated = 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 const u = await db
.insert(schema.users) .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 }); .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]); generatedUserData.push(u[0]);
i++; 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, "when": 1722853523826,
"tag": "0002_chemical_gambit", "tag": "0002_chemical_gambit",
"breakpoints": true "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 listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']);
export const users = pgTable('users', { export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom().notNull(),
firstname: varchar('firstname', { length: 255 }).notNull(), firstname: varchar('firstname', { length: 255 }).notNull(),
lastname: varchar('lastname', { length: 255 }).notNull(), lastname: varchar('lastname', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(), email: varchar('email', { length: 255 }).notNull().unique(),
@ -16,7 +16,8 @@ export const users = pgTable('users', {
companyName: varchar('companyName', { length: 255 }), companyName: varchar('companyName', { length: 255 }),
companyOverview: text('companyOverview'), companyOverview: text('companyOverview'),
companyWebsite: varchar('companyWebsite', { length: 255 }), companyWebsite: varchar('companyWebsite', { length: 255 }),
companyLocation: varchar('companyLocation', { length: 255 }), city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
offeredServices: text('offeredServices'), offeredServices: text('offeredServices'),
areasServed: jsonb('areasServed').$type<AreasServed[]>(), areasServed: jsonb('areasServed').$type<AreasServed[]>(),
hasProfile: boolean('hasProfile'), hasProfile: boolean('hasProfile'),
@ -33,7 +34,7 @@ export const users = pgTable('users', {
}); });
export const businesses = pgTable('businesses', { export const businesses = pgTable('businesses', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom().notNull(),
email: varchar('email', { length: 255 }).references(() => users.email), email: varchar('email', { length: 255 }).references(() => users.email),
type: varchar('type', { length: 255 }), type: varchar('type', { length: 255 }),
title: varchar('title', { length: 255 }), title: varchar('title', { length: 255 }),
@ -69,7 +70,7 @@ export const businesses = pgTable('businesses', {
}); });
export const commercials = pgTable('commercials', { export const commercials = pgTable('commercials', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom().notNull(),
serialId: serial('serialId'), serialId: serial('serialId'),
email: varchar('email', { length: 255 }).references(() => users.email), email: varchar('email', { length: 255 }).references(() => users.email),
type: varchar('type', { length: 255 }), type: varchar('type', { length: 255 }),

View File

@ -53,16 +53,18 @@ export class GeoService {
result.push({ result.push({
id: city.id, id: city.id,
city: city.name, city: city.name,
state: state.name, state: state.state_code,
state_code: 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 }> { 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_code: string }> = []; const results: Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> = [];
const lowercasePrefix = prefix.toLowerCase(); const lowercasePrefix = prefix.toLowerCase();
@ -74,7 +76,7 @@ export class GeoService {
id: state.id.toString(), id: state.id.toString(),
name: state.name, name: state.name,
type: 'state', type: 'state',
state_code: state.state_code, state: state.state_code,
}); });
} }
@ -85,7 +87,7 @@ export class GeoService {
id: city.id.toString(), id: city.id.toString(),
name: city.name, name: city.name,
type: 'city', 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) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city); 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) { if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(businesses.type, criteria.types)); 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) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city); 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) { if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(schema.commercials.type, criteria.types)); 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'), registerNo: z.string().nonempty('Registration number is required'),
state: z.string().nonempty('State is required'), state: z.string().nonempty('State is required'),
}); });
export const GeoSchema = z.object({
const phoneRegex = /^\+1 \(\d{3}\) \d{3}-\d{4}$/; 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 export const UserSchema = z
.object({ .object({
@ -123,7 +142,7 @@ export const UserSchema = z
companyName: z.string().optional().nullable(), companyName: z.string().optional().nullable(),
companyOverview: z.string().optional().nullable(), companyOverview: z.string().optional().nullable(),
companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(), companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
companyLocation: z.string().optional().nullable(), companyLocation: GeoSchema,
offeredServices: z.string().optional().nullable(), offeredServices: z.string().optional().nullable(),
areasServed: z.array(AreasServedSchema).optional().nullable(), areasServed: z.array(AreasServedSchema).optional().nullable(),
hasProfile: z.boolean().optional().nullable(), hasProfile: z.boolean().optional().nullable(),
@ -134,8 +153,6 @@ export const UserSchema = z
customerSubType: CustomerSubTypeEnum.optional().nullable(), customerSubType: CustomerSubTypeEnum.optional().nullable(),
created: z.date().optional().nullable(), created: z.date().optional().nullable(),
updated: z.date().optional().nullable(), updated: z.date().optional().nullable(),
latitude: z.number().optional().nullable(),
longitude: z.number().optional().nullable(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.customerType === 'professional') { if (data.customerType === 'professional') {
@ -150,7 +167,7 @@ export const UserSchema = z
if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) { if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, 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'], path: ['phoneNumber'],
}); });
} }
@ -236,8 +253,22 @@ export const BusinessListingSchema = z.object({
updated: z.date(), updated: z.date(),
visits: z.number().int().positive().optional().nullable(), visits: z.number().int().positive().optional().nullable(),
lastVisit: z.date().optional().nullable(), lastVisit: z.date().optional().nullable(),
latitude: z.number().optional().nullable(), latitude: z.number().refine(
longitude: z.number().optional().nullable(), 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>; export type BusinessListing = z.infer<typeof BusinessListingSchema>;
@ -268,8 +299,22 @@ export const CommercialPropertyListingSchema = z
updated: z.date(), updated: z.date(),
visits: z.number().int().positive().nullable().optional(), visits: z.number().int().positive().nullable().optional(),
lastVisit: z.date().nullable().optional(), lastVisit: z.date().nullable().optional(),
latitude: z.number().nullable().optional(), latitude: z.number().refine(
longitude: z.number().nullable().optional(), 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(); .strict();

View File

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

View File

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

View File

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

View File

@ -1,10 +1,8 @@
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { z } from 'zod';
import { businesses, commercials, users } from './drizzle/schema.js'; 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_KM = 6371; // Erdradius in Kilometern
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
export function convertStringToNullUndefined(value) { export function convertStringToNullUndefined(value) {
// Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung // Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung
const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase(); 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): { type DrizzleUser = typeof users.$inferSelect; //Partial<InferInsertModel<typeof users>>;
email: string;
firstname: string; export function convertUserToDrizzleUser(user: Partial<User>): DrizzleUser {
lastname: string; const { companyLocation, ...restUser } = user;
phoneNumber?: string;
description?: string; // Ensure all required fields are present
companyName?: string; if (!user.id || !user.email || !user.firstname || !user.lastname) {
companyOverview?: string; throw new Error('Missing required fields: id, email, firstname, or lastname');
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;
return { return {
...drizzleUser, id: user.id,
email: drizzleUser.email, email: user.email,
firstname: drizzleUser.firstname, firstname: user.firstname,
lastname: drizzleUser.lastname, 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", "memoize-one": "^6.0.0",
"ngx-currency": "^18.0.0", "ngx-currency": "^18.0.0",
"ngx-image-cropper": "^8.0.0", "ngx-image-cropper": "^8.0.0",
"ngx-mask": "^18.0.0",
"ngx-quill": "^26.0.5", "ngx-quill": "^26.0.5",
"on-change": "^5.0.1", "on-change": "^5.0.1",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",

View File

@ -11,12 +11,45 @@
@if (loadingService.isLoading$ | async) { @if (loadingService.isLoading$ | async) {
<div class="spinner-overlay"> <div class="spinner-overlay">
<div class="spinner-container"> <div class="spinner-container">
@let loadingText = (loadingService.loadingText$ | async); @let loadingText = (loadingService.loadingText$ | async); @if(loadingText){
<!-- @if(loadingService.loadingText$ | async){ -->
<div class="spinner-text">{{ loadingText }}</div> <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> </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-message-container></app-message-container>
<app-search-modal></app-search-modal> <app-search-modal></app-search-modal>
<app-confirmation></app-confirmation>

View File

@ -1,25 +1,25 @@
.progress-spinner { // .progress-spinner {
position: fixed; // position: fixed;
z-index: 999; // z-index: 999;
top: 0; // top: 0;
left: 0; // left: 0;
bottom: 0; // bottom: 0;
right: 0; // right: 0;
display: flex; // display: flex;
flex-direction: column; // flex-direction: column;
align-items: center; // align-items: center;
} // }
.progress-spinner:before { // .progress-spinner:before {
content: ''; // content: '';
display: block; // display: block;
position: fixed; // position: fixed;
top: 0; // top: 0;
left: 0; // left: 0;
width: 100%; // width: 100%;
height: 100%; // height: 100%;
background-color: rgba(0, 0, 0, 0.3); // background-color: rgba(0, 0, 0, 0.3);
} // }
.spinner-text { .spinner-text {
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */ margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
font-size: 20px; /* Schriftgröße nach Bedarf 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 { filter } from 'rxjs/operators';
import build from '../build'; 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 { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component'; import { HeaderComponent } from './components/header/header.component';
import { MessageContainerComponent } from './components/message/message-container.component'; import { MessageContainerComponent } from './components/message/message-container.component';
@ -15,7 +17,7 @@ import { UserService } from './services/user.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent], imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent],
providers: [], providers: [],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
@ -25,7 +27,14 @@ export class AppComponent {
title = 'bizmatch'; title = 'bizmatch';
actualRoute = ''; 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(() => { this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
let currentRoute = this.activatedRoute.root; let currentRoute = this.activatedRoute.root;
while (currentRoute.children[0] !== undefined) { while (currentRoute.children[0] !== undefined) {
@ -49,13 +58,6 @@ export class AppComponent {
} }
showVersionDialog() { showVersionDialog() {
// this.confirmationService.confirm({ this.confirmationService.showConfirmation({ message: `App Version: ${this.build.timestamp}`, buttons: 'none' });
// target: event.target as EventTarget,
// message: `App Version: ${this.build.timestamp}`,
// header: 'Version Info',
// icon: 'pi pi-info-circle',
// accept: () => {},
// reject: () => {},
// });
} }
} }

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"> <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" /> <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> </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 <button
(click)="confirmationService.accept()" (click)="confirmationService.accept()"
type="button" type="button"
@ -39,6 +41,7 @@ import { ConfirmationService } from './confirmation.service';
> >
No, cancel No, cancel
</button> </button>
}
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -1,7 +1,7 @@
import { CdkDrag, CdkDragEnd, CdkDragMove, DragDropModule, DragRef, moveItemInArray } from '@angular/cdk/drag-drop'; import { CdkDrag, CdkDragEnd, CdkDragMove, DragDropModule, DragRef, moveItemInArray } from '@angular/cdk/drag-drop';
import { _getShadowRoot } from '@angular/cdk/platform'; import { _getShadowRoot } from '@angular/cdk/platform';
import { CommonModule } from '@angular/common'; 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 { CommercialPropertyListing } from '../../../../../bizmatch-server/src/models/db.model';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@Component({ @Component({
@ -14,12 +14,11 @@ import { environment } from '../../../environments/environment';
export class DragDropMixedComponent { export class DragDropMixedComponent {
@ViewChild('_container') _container!: ElementRef<HTMLDivElement>; @ViewChild('_container') _container!: ElementRef<HTMLDivElement>;
@ViewChildren(CdkDrag) _drags!: QueryList<CdkDrag>; @ViewChildren(CdkDrag) _drags!: QueryList<CdkDrag>;
@Input() ts: number;
listing = input<CommercialPropertyListing>(); listing = input<CommercialPropertyListing>();
imageOrderChanged = output<string[]>(); imageOrderChanged = output<string[]>();
imageToDelete = output<string>(); imageToDelete = output<string>();
env = environment; env = environment;
ts = new Date().getTime();
items: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9]; items: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];
private _cachedItems: 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; private _containerStyle: CSSStyleDeclaration | null = null;
public isAnimationActive = false; public isAnimationActive = false;
constructor(private cdr: ChangeDetectorRef) {}
ngOnChanges() { ngOnChanges() {
this.items = this.listing()?.imageOrder; this.items = this.listing()?.imageOrder;
this._cachedItems = this.items.slice(); this._cachedItems = this.items.slice();
} }
ngAfterViewInit() {
// Führen Sie einen zusätzlichen Change Detection-Zyklus durch
this.cdr.detectChanges();
}
getImageUrl(image: string): string { getImageUrl(image: string): string {
return `${this.env.imageBaseUrl}/pictures/property/${this.listing().imagePath}/${this.listing().serialId}/${image}?_ts=${this.ts}`; 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.imageChangedEvent = null;
this.croppedImage = null; this.croppedImage = null;
this.showModal = false; this.showModal = false;
this.fileInput.nativeElement.value = '';
this.uploadFinished.emit({ success: false, type: this.uploadParams.type }); this.uploadFinished.emit({ success: false, type: this.uploadParams.type });
} }
uploadImage() { async uploadImage() {
if (this.croppedImage) { if (this.croppedImage) {
this.imageService.uploadImage(this.croppedImage, this.uploadParams.type, this.uploadParams.imagePath, this.uploadParams.serialId).subscribe( await this.imageService.uploadImage(this.croppedImage, this.uploadParams.type, this.uploadParams.imagePath, this.uploadParams.serialId);
response => { this.closeModal();
console.log('Upload successful', response); this.uploadFinished.emit({ success: true, type: this.uploadParams.type });
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 });
},
);
} }
} }

View File

@ -39,7 +39,7 @@
(ngModelChange)="setCity($event)" (ngModelChange)="setCity($event)"
> >
@for (city of cities$ | async; track city.id) { @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> </ng-select>
</div> </div>

View File

@ -98,7 +98,7 @@ export class SearchModalComponent {
setCity(city) { setCity(city) {
if (city) { if (city) {
this.criteria.city = city.city; this.criteria.city = city.city;
this.criteria.state = city.state_code; this.criteria.state = city.state;
} else { } else {
this.criteria.city = null; this.criteria.city = null;
this.criteria.radius = 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()" (blur)="onTouched()"
[attr.name]="name" [attr.name]="name"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" 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> </div>

View File

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

View File

@ -84,7 +84,7 @@ export class DetailsBusinessListingComponent {
this.keycloakUser = map2User(token); this.keycloakUser = map2User(token);
if (this.keycloakUser) { if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email); 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.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
this.listingUser = await this.userService.getByMail(this.listing.email); this.listingUser = await this.userService.getByMail(this.listing.email);

View File

@ -86,7 +86,7 @@ export class DetailsCommercialPropertyListingComponent {
this.keycloakUser = map2User(token); this.keycloakUser = map2User(token);
if (this.keycloakUser) { if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email); 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.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
this.listingUser = await this.userService.getByMail(this.listing.email); this.listingUser = await this.userService.getByMail(this.listing.email);

View File

@ -114,7 +114,7 @@
groupBy="type" groupBy="type"
> >
@for (city of cities$ | async; track city.id) { @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> </ng-select>
</div> </div>

View File

@ -154,10 +154,10 @@ export class HomeComponent {
setCityOrState(cityOrState: CityAndStateResult) { setCityOrState(cityOrState: CityAndStateResult) {
if (cityOrState) { if (cityOrState) {
if (cityOrState.type === 'state') { if (cityOrState.type === 'state') {
this.criteria.state = cityOrState.state_code; this.criteria.state = cityOrState.state;
} else { } else {
this.criteria.city = cityOrState.name; this.criteria.city = cityOrState.name;
this.criteria.state = cityOrState.state_code; this.criteria.state = cityOrState.state;
this.criteria.searchType = 'radius'; this.criteria.searchType = 'radius';
this.criteria.radius = 20; 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> <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" /> <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> --> </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 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>
<!-- <div> <!-- <div>

View File

@ -18,6 +18,7 @@ import { ImageCropAndUploadComponent, UploadReponse } from '../../../components/
import { MessageComponent } from '../../../components/message/message.component'; import { MessageComponent } from '../../../components/message/message.component';
import { MessageService } from '../../../components/message/message.service'; import { MessageService } from '../../../components/message/message.service';
import { TooltipComponent } from '../../../components/tooltip/tooltip.component'; 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 { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
import { ValidatedQuillComponent } from '../../../components/validated-quill/validated-quill.component'; import { ValidatedQuillComponent } from '../../../components/validated-quill/validated-quill.component';
import { ValidatedSelectComponent } from '../../../components/validated-select/validated-select.component'; import { ValidatedSelectComponent } from '../../../components/validated-select/validated-select.component';
@ -47,6 +48,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
ValidatedInputComponent, ValidatedInputComponent,
ValidatedSelectComponent, ValidatedSelectComponent,
ValidatedQuillComponent, ValidatedQuillComponent,
ValidatedCityComponent,
TooltipComponent, TooltipComponent,
], ],
providers: [TitleCasePipe], providers: [TitleCasePipe],
@ -167,7 +169,7 @@ export class AccountComponent {
async search(event: AutoCompleteCompleteEvent) { async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query)); 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() { addLicence() {
this.user.licensedIn.push({ registerNo: '', state: '' }); this.user.licensedIn.push({ registerNo: '', state: '' });
@ -204,7 +206,7 @@ export class AccountComponent {
} }
} }
async deleteConfirm(type: 'profile' | 'logo') { 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 (confirmed) {
if (type === 'profile') { if (type === 'profile') {
this.user.hasProfile = false; this.user.hasProfile = false;

View File

@ -127,7 +127,7 @@
</div> </div>
</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> -->
</div> </div>
@if (mode!=='create'){ @if (mode!=='create'){

View File

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

View File

@ -39,7 +39,7 @@ export class EmailUsComponent {
this.keycloakUser = map2User(token); this.keycloakUser = map2User(token);
if (this.keycloakUser) { if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email); 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() { ngOnDestroy() {

View File

@ -54,7 +54,7 @@ export class MyListingComponent {
} }
async confirm(listing: ListingType) { 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) { if (confirmed) {
// this.messageService.showMessage('Listing has been deleted'); // this.messageService.showMessage('Listing has been deleted');
this.deleteListing(listing); this.deleteListing(listing);

View File

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

View File

@ -1,13 +1,15 @@
import { Injectable } from '@angular/core'; 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({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class LoadingService { export class LoadingService {
public loading$ = new BehaviorSubject<string[]>([]); private loading$ = new BehaviorSubject<string[]>([]);
private loadingTextSubject = new BehaviorSubject<string | null>(null); 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(); loadingText$: Observable<string | null> = this.loadingTextSubject.asObservable();
public isLoading$ = this.loading$.asObservable().pipe( public isLoading$ = this.loading$.asObservable().pipe(
@ -17,13 +19,15 @@ export class LoadingService {
shareReplay(1), shareReplay(1),
); );
public startLoading(type: string, request?: string): void { public startLoading(type: string, url?: string): void {
if (!this.loading$.value.includes(type)) { if (this.shouldShowLoading(url)) {
this.loading$.next(this.loading$.value.concat(type)); if (!this.loading$.value.includes(type)) {
if (type === 'uploadImage' || request?.includes('uploadImage') || request?.includes('uploadPropertyPicture') || request?.includes('uploadProfile') || request?.includes('uploadCompanyLogo')) { this.loading$.next(this.loading$.value.concat(type));
this.loadingTextSubject.next("Please wait - we're processing your image..."); if (this.isImageUpload(type, url)) {
} else { this.loadingTextSubject.next("Please wait - we're processing your image...");
this.loadingTextSubject.next(null); } else {
this.loadingTextSubject.next(null);
}
} }
} }
} }
@ -34,4 +38,13 @@ export class LoadingService {
this.loadingTextSubject.next(null); 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');
}
} }