Issues gitea
This commit is contained in:
parent
897ab1ff77
commit
09e7ce59a9
|
|
@ -0,0 +1,119 @@
|
||||||
|
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
import { sql, eq, and } from 'drizzle-orm';
|
||||||
|
import * as schema from '../src/drizzle/schema';
|
||||||
|
import { users_json } from '../src/drizzle/schema';
|
||||||
|
|
||||||
|
// Mock JwtUser
|
||||||
|
interface JwtUser {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic from UserService.addFavorite
|
||||||
|
async function addFavorite(db: NodePgDatabase<typeof schema>, id: string, user: JwtUser) {
|
||||||
|
console.log(`[Action] Adding favorite. Target ID: ${id}, Favoriter Email: ${user.email}`);
|
||||||
|
await db
|
||||||
|
.update(schema.users_json)
|
||||||
|
.set({
|
||||||
|
data: sql`jsonb_set(${schema.users_json.data}, '{favoritesForUser}',
|
||||||
|
coalesce((${schema.users_json.data}->'favoritesForUser')::jsonb, '[]'::jsonb) || to_jsonb(${user.email}::text))`,
|
||||||
|
} as any)
|
||||||
|
.where(eq(schema.users_json.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic from UserService.getFavoriteUsers
|
||||||
|
async function getFavoriteUsers(db: NodePgDatabase<typeof schema>, user: JwtUser) {
|
||||||
|
console.log(`[Action] Fetching favorites for ${user.email}`);
|
||||||
|
|
||||||
|
// Corrected query using `?` operator (matches array element check)
|
||||||
|
const data = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.users_json)
|
||||||
|
.where(sql`${schema.users_json.data}->'favoritesForUser' ? ${user.email}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic from UserService.deleteFavorite
|
||||||
|
async function deleteFavorite(db: NodePgDatabase<typeof schema>, id: string, user: JwtUser) {
|
||||||
|
console.log(`[Action] Removing favorite. Target ID: ${id}, Favoriter Email: ${user.email}`);
|
||||||
|
await db
|
||||||
|
.update(schema.users_json)
|
||||||
|
.set({
|
||||||
|
data: sql`jsonb_set(${schema.users_json.data}, '{favoritesForUser}',
|
||||||
|
(SELECT coalesce(jsonb_agg(elem), '[]'::jsonb)
|
||||||
|
FROM jsonb_array_elements(coalesce(${schema.users_json.data}->'favoritesForUser', '[]'::jsonb)) AS elem
|
||||||
|
WHERE elem::text != to_jsonb(${user.email}::text)::text))`,
|
||||||
|
} as any)
|
||||||
|
.where(eq(schema.users_json.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('═══════════════════════════════════════════════════════');
|
||||||
|
console.log(' FAVORITES REPRODUCTION SCRIPT');
|
||||||
|
console.log('═══════════════════════════════════════════════════════\n');
|
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/bizmatch';
|
||||||
|
const pool = new Pool({ connectionString });
|
||||||
|
const db = drizzle(pool, { schema });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Find a "professional" user to be the TARGET listing
|
||||||
|
// filtering by customerType = 'professional' inside the jsonb data
|
||||||
|
const targets = await db.select().from(users_json).limit(1);
|
||||||
|
|
||||||
|
if (targets.length === 0) {
|
||||||
|
console.error("No users found in DB to test with.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUser = targets[0];
|
||||||
|
console.log(`Found target user: ID=${targetUser.id}, Email=${targetUser.email}`);
|
||||||
|
|
||||||
|
// 2. Define a "favoriter" user (doesn't need to exist in DB for the logic to work, but better if it's realistic)
|
||||||
|
// We'll just use a dummy email or one from DB if available.
|
||||||
|
const favoriterEmail = 'test-repro-favoriter@example.com';
|
||||||
|
const favoriter: JwtUser = { email: favoriterEmail };
|
||||||
|
|
||||||
|
// 3. Clear any existing favorite for this pair first
|
||||||
|
await deleteFavorite(db, targetUser.id, favoriter);
|
||||||
|
|
||||||
|
// 4. Add Favorite
|
||||||
|
await addFavorite(db, targetUser.id, favoriter);
|
||||||
|
|
||||||
|
// 5. Verify it was added by checking the raw data
|
||||||
|
const updatedTarget = await db.select().from(users_json).where(eq(users_json.id, targetUser.id));
|
||||||
|
const favoritesData = (updatedTarget[0].data as any).favoritesForUser;
|
||||||
|
console.log(`\n[Check] Raw favoritesForUser data on target:`, favoritesData);
|
||||||
|
|
||||||
|
if (!favoritesData || !favoritesData.includes(favoriterEmail)) {
|
||||||
|
console.error("❌ Add Favorite FAILED. Email not found in favoritesForUser array.");
|
||||||
|
} else {
|
||||||
|
console.log("✅ Add Favorite SUCCESS. Email found in JSON.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Test retrieval using the getFavoriteUsers query
|
||||||
|
const retrievedFavorites = await getFavoriteUsers(db, favoriter);
|
||||||
|
console.log(`\n[Check] retrievedFavorites count: ${retrievedFavorites.length}`);
|
||||||
|
|
||||||
|
const found = retrievedFavorites.find(u => u.id === targetUser.id);
|
||||||
|
if (found) {
|
||||||
|
console.log("✅ Get Favorites SUCCESS. Target user returned in query.");
|
||||||
|
} else {
|
||||||
|
console.log("❌ Get Favorites FAILED. Target user NOT returned by query.");
|
||||||
|
console.log("Query used: favoritesForUser @> [email]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Cleanup
|
||||||
|
await deleteFavorite(db, targetUser.id, favoriter);
|
||||||
|
console.log("\n[Cleanup] Removed test favorite.");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Script failed:', error);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -13,7 +13,13 @@ export class BusinessListingsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly listingsService: BusinessListingService,
|
private readonly listingsService: BusinessListingService,
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@Post('favorites/all')
|
||||||
|
async findFavorites(@Request() req): Promise<any> {
|
||||||
|
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(OptionalAuthGuard)
|
@UseGuards(OptionalAuthGuard)
|
||||||
@Get(':slugOrId')
|
@Get(':slugOrId')
|
||||||
|
|
@ -21,11 +27,7 @@ export class BusinessListingsController {
|
||||||
// Support both slug (e.g., "restaurant-austin-tx-a3f7b2c1") and UUID
|
// Support both slug (e.g., "restaurant-austin-tx-a3f7b2c1") and UUID
|
||||||
return await this.listingsService.findBusinessBySlugOrId(slugOrId, req.user as JwtUser);
|
return await this.listingsService.findBusinessBySlugOrId(slugOrId, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Get('favorites/all')
|
|
||||||
async findFavorites(@Request() req): Promise<any> {
|
|
||||||
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
|
||||||
}
|
|
||||||
@UseGuards(OptionalAuthGuard)
|
@UseGuards(OptionalAuthGuard)
|
||||||
@Get('user/:userid')
|
@Get('user/:userid')
|
||||||
async findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
async findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,13 @@ export class CommercialPropertyListingsController {
|
||||||
private readonly listingsService: CommercialPropertyService,
|
private readonly listingsService: CommercialPropertyService,
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@Post('favorites/all')
|
||||||
|
async findFavorites(@Request() req): Promise<any> {
|
||||||
|
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(OptionalAuthGuard)
|
@UseGuards(OptionalAuthGuard)
|
||||||
@Get(':slugOrId')
|
@Get(':slugOrId')
|
||||||
|
|
@ -24,12 +30,6 @@ export class CommercialPropertyListingsController {
|
||||||
return await this.listingsService.findCommercialBySlugOrId(slugOrId, req.user as JwtUser);
|
return await this.listingsService.findCommercialBySlugOrId(slugOrId, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Get('favorites/all')
|
|
||||||
async findFavorites(@Request() req): Promise<any> {
|
|
||||||
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(OptionalAuthGuard)
|
@UseGuards(OptionalAuthGuard)
|
||||||
@Get('user/:email')
|
@Get('user/:email')
|
||||||
async findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
async findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { UserService } from '../user/user.service';
|
||||||
import { BrokerListingsController } from './broker-listings.controller';
|
import { BrokerListingsController } from './broker-listings.controller';
|
||||||
import { BusinessListingsController } from './business-listings.controller';
|
import { BusinessListingsController } from './business-listings.controller';
|
||||||
import { CommercialPropertyListingsController } from './commercial-property-listings.controller';
|
import { CommercialPropertyListingsController } from './commercial-property-listings.controller';
|
||||||
|
import { UserListingsController } from './user-listings.controller';
|
||||||
|
|
||||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||||
import { GeoModule } from '../geo/geo.module';
|
import { GeoModule } from '../geo/geo.module';
|
||||||
|
|
@ -16,7 +17,7 @@ import { UnknownListingsController } from './unknown-listings.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DrizzleModule, AuthModule, GeoModule,FirebaseAdminModule],
|
imports: [DrizzleModule, AuthModule, GeoModule,FirebaseAdminModule],
|
||||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController, UserListingsController],
|
||||||
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
|
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
|
||||||
exports: [BusinessListingService, CommercialPropertyService],
|
exports: [BusinessListingService, CommercialPropertyService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Controller, Delete, Param, Post, Request, UseGuards } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '../jwt-auth/auth.guard';
|
||||||
|
import { JwtUser } from '../models/main.model';
|
||||||
|
import { UserService } from '../user/user.service';
|
||||||
|
|
||||||
|
@Controller('listings/user')
|
||||||
|
export class UserListingsController {
|
||||||
|
constructor(private readonly userService: UserService) { }
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@Post('favorite/:id')
|
||||||
|
async addFavorite(@Request() req, @Param('id') id: string) {
|
||||||
|
await this.userService.addFavorite(id, req.user as JwtUser);
|
||||||
|
return { success: true, message: 'Added to favorites' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@Delete('favorite/:id')
|
||||||
|
async deleteFavorite(@Request() req, @Param('id') id: string) {
|
||||||
|
await this.userService.deleteFavorite(id, req.user as JwtUser);
|
||||||
|
return { success: true, message: 'Removed from favorites' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@Post('favorites/all')
|
||||||
|
async getFavorites(@Request() req) {
|
||||||
|
return await this.userService.getFavoriteUsers(req.user as JwtUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
|
import { Client } from 'pg';
|
||||||
|
import * as schema from '../drizzle/schema';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.PG_CONNECTION,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await client.connect();
|
||||||
|
const db = drizzle(client, { schema });
|
||||||
|
|
||||||
|
const testEmail = 'knuth.timo@gmail.com';
|
||||||
|
const targetEmail = 'target.user@example.com';
|
||||||
|
|
||||||
|
console.log('--- Starting Debug Script ---');
|
||||||
|
|
||||||
|
// 1. Simulate finding a user to favorite (using a dummy or existing one)
|
||||||
|
// For safety, let's just query existing users to see if any have favorites set
|
||||||
|
const usersWithFavorites = await db.select({
|
||||||
|
id: schema.users_json.id,
|
||||||
|
email: schema.users_json.email,
|
||||||
|
favorites: sql`${schema.users_json.data}->'favoritesForUser'`
|
||||||
|
}).from(schema.users_json);
|
||||||
|
|
||||||
|
console.log(`Found ${usersWithFavorites.length} users.`);
|
||||||
|
|
||||||
|
const usersWithAnyFavorites = usersWithFavorites.filter(u => u.favorites !== null);
|
||||||
|
console.log(`Users with 'favoritesForUser' field:`, JSON.stringify(usersWithAnyFavorites, null, 2));
|
||||||
|
|
||||||
|
// 2. Test the specific WHERE clause used in the service
|
||||||
|
// .where(sql`${schema.users_json.data}->'favoritesForUser' @> ${JSON.stringify([user.email])}::jsonb`);
|
||||||
|
|
||||||
|
console.log(`Testing query for email: ${testEmail}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db
|
||||||
|
.select({
|
||||||
|
id: schema.users_json.id,
|
||||||
|
email: schema.users_json.email
|
||||||
|
})
|
||||||
|
.from(schema.users_json)
|
||||||
|
.where(sql`${schema.users_json.data}->'favoritesForUser' @> ${JSON.stringify([testEmail])}::jsonb`);
|
||||||
|
|
||||||
|
console.log('Query Result:', result);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Query Failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
|
@ -19,7 +19,7 @@ export class UserService {
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
private geoService: GeoService,
|
private geoService: GeoService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
private getWhereConditions(criteria: UserListingCriteria): SQL[] {
|
private getWhereConditions(criteria: UserListingCriteria): SQL[] {
|
||||||
const whereConditions: SQL[] = [];
|
const whereConditions: SQL[] = [];
|
||||||
|
|
@ -158,4 +158,34 @@ export class UserService {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addFavorite(id: string, user: JwtUser): Promise<void> {
|
||||||
|
await this.conn
|
||||||
|
.update(schema.users_json)
|
||||||
|
.set({
|
||||||
|
data: sql`jsonb_set(${schema.users_json.data}, '{favoritesForUser}',
|
||||||
|
coalesce((${schema.users_json.data}->'favoritesForUser')::jsonb, '[]'::jsonb) || to_jsonb(${user.email}::text))`,
|
||||||
|
} as any)
|
||||||
|
.where(eq(schema.users_json.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFavorite(id: string, user: JwtUser): Promise<void> {
|
||||||
|
await this.conn
|
||||||
|
.update(schema.users_json)
|
||||||
|
.set({
|
||||||
|
data: sql`jsonb_set(${schema.users_json.data}, '{favoritesForUser}',
|
||||||
|
(SELECT coalesce(jsonb_agg(elem), '[]'::jsonb)
|
||||||
|
FROM jsonb_array_elements(coalesce(${schema.users_json.data}->'favoritesForUser', '[]'::jsonb)) AS elem
|
||||||
|
WHERE elem::text != to_jsonb(${user.email}::text)::text))`,
|
||||||
|
} as any)
|
||||||
|
.where(eq(schema.users_json.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFavoriteUsers(user: JwtUser): Promise<User[]> {
|
||||||
|
const data = await this.conn
|
||||||
|
.select()
|
||||||
|
.from(schema.users_json)
|
||||||
|
.where(sql`${schema.users_json.data}->'favoritesForUser' ? ${user.email}`);
|
||||||
|
return data.map(u => ({ id: u.id, email: u.email, ...(u.data as User) }) as User);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,18 @@
|
||||||
<div class="p-6 flex flex-col lg:flex-row">
|
<div class="p-6 flex flex-col lg:flex-row">
|
||||||
<!-- Left column -->
|
<!-- Left column -->
|
||||||
<div class="w-full lg:w-1/2 pr-0 lg:pr-6">
|
<div class="w-full lg:w-1/2 pr-0 lg:pr-6">
|
||||||
<h1 class="text-2xl font-bold mb-4">{{ listing.title }}</h1>
|
<h1 class="text-2xl font-bold mb-4 break-words">{{ listing.title }}</h1>
|
||||||
<p class="mb-4" [innerHTML]="description"></p>
|
<p class="mb-4 break-words" [innerHTML]="description"></p>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div *ngFor="let detail of listingDetails; let i = index" class="flex flex-col sm:flex-row"
|
<div *ngFor="let detail of listingDetails; let i = index" class="flex flex-col sm:flex-row"
|
||||||
[ngClass]="{ 'bg-neutral-100': i % 2 === 0 }">
|
[ngClass]="{ 'bg-neutral-100': i % 2 === 0 }">
|
||||||
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
|
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
|
||||||
|
|
||||||
<div class="w-full sm:w-2/3 p-2" *ngIf="!detail.isHtml && !detail.isListingBy">{{ detail.value }}</div>
|
<div class="w-full sm:w-2/3 p-2 break-words" *ngIf="!detail.isHtml && !detail.isListingBy">{{ detail.value
|
||||||
|
}}</div>
|
||||||
|
|
||||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value"
|
<div class="w-full sm:w-2/3 p-2 flex space-x-2 break-words" [innerHTML]="detail.value"
|
||||||
*ngIf="detail.isHtml && !detail.isListingBy"></div>
|
*ngIf="detail.isHtml && !detail.isListingBy"></div>
|
||||||
|
|
||||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" *ngIf="detail.isListingBy && listingUser">
|
<div class="w-full sm:w-2/3 p-2 flex space-x-2" *ngIf="detail.isListingBy && listingUser">
|
||||||
|
|
@ -32,7 +33,8 @@
|
||||||
listingUser.lastname }}</a>
|
listingUser.lastname }}</a>
|
||||||
<img *ngIf="listing.imageName"
|
<img *ngIf="listing.imageName"
|
||||||
ngSrc="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}"
|
ngSrc="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}"
|
||||||
class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" width="100" height="30" alt="Business logo for {{ listingUser.firstname }} {{ listingUser.lastname }}" />
|
class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" width="100" height="30"
|
||||||
|
alt="Business logo for {{ listingUser.firstname }} {{ listingUser.lastname }}" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -48,7 +50,7 @@
|
||||||
} @if(user){
|
} @if(user){
|
||||||
<div class="inline">
|
<div class="inline">
|
||||||
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||||
(click)="save()" [disabled]="listing.favoritesForUser.includes(user.email)">
|
(click)="toggleFavorite()">
|
||||||
<i class="fa-regular fa-heart"></i>
|
<i class="fa-regular fa-heart"></i>
|
||||||
@if(listing.favoritesForUser.includes(user.email)){
|
@if(listing.favoritesForUser.includes(user.email)){
|
||||||
<span class="ml-2">Saved ...</span>
|
<span class="ml-2">Saved ...</span>
|
||||||
|
|
@ -142,10 +144,13 @@
|
||||||
<h2 class="text-2xl font-bold mb-6 text-gray-900">Frequently Asked Questions</h2>
|
<h2 class="text-2xl font-bold mb-6 text-gray-900">Frequently Asked Questions</h2>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@for (faq of businessFAQs; track $index) {
|
@for (faq of businessFAQs; track $index) {
|
||||||
<details class="group border border-gray-200 rounded-lg overflow-hidden hover:border-primary-300 transition-colors">
|
<details
|
||||||
<summary class="flex items-center justify-between cursor-pointer p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
|
class="group border border-gray-200 rounded-lg overflow-hidden hover:border-primary-300 transition-colors">
|
||||||
|
<summary
|
||||||
|
class="flex items-center justify-between cursor-pointer p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
|
||||||
<h3 class="text-lg font-semibold text-gray-900">{{ faq.question }}</h3>
|
<h3 class="text-lg font-semibold text-gray-900">{{ faq.question }}</h3>
|
||||||
<svg class="w-5 h-5 text-gray-600 group-open:rotate-180 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 text-gray-600 group-open:rotate-180 transition-transform" fill="none"
|
||||||
|
stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
@ -157,7 +162,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 p-4 bg-primary-50 border-l-4 border-primary-500 rounded">
|
<div class="mt-6 p-4 bg-primary-50 border-l-4 border-primary-500 rounded">
|
||||||
<p class="text-sm text-gray-700">
|
<p class="text-sm text-gray-700">
|
||||||
<strong class="text-primary-700">Have more questions?</strong> Contact the seller directly using the form above or reach out to our support team for assistance.
|
<strong class="text-primary-700">Have more questions?</strong> Contact the seller directly using the form
|
||||||
|
above or reach out to our support team for assistance.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -379,13 +379,27 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
async save() {
|
async toggleFavorite() {
|
||||||
await this.listingsService.addToFavorites(this.listing.id, 'business');
|
try {
|
||||||
this.listing.favoritesForUser.push(this.user.email);
|
const isFavorited = this.listing.favoritesForUser.includes(this.user.email);
|
||||||
this.auditService.createEvent(this.listing.id, 'favorite', this.user?.email);
|
|
||||||
}
|
if (isFavorited) {
|
||||||
isAlreadyFavorite() {
|
// Remove from favorites
|
||||||
return this.listing.favoritesForUser.includes(this.user.email);
|
await this.listingsService.removeFavorite(this.listing.id, 'business');
|
||||||
|
this.listing.favoritesForUser = this.listing.favoritesForUser.filter(
|
||||||
|
email => email !== this.user.email
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Add to favorites
|
||||||
|
await this.listingsService.addToFavorites(this.listing.id, 'business');
|
||||||
|
this.listing.favoritesForUser.push(this.user.email);
|
||||||
|
this.auditService.createEvent(this.listing.id, 'favorite', this.user?.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cdref.detectChanges();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling favorite:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async showShareByEMail() {
|
async showShareByEMail() {
|
||||||
const result = await this.emailService.showShareByEMail({
|
const result = await this.emailService.showShareByEMail({
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@
|
||||||
<div class="bg-white drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg rounded-lg overflow-hidden">
|
<div class="bg-white drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg rounded-lg overflow-hidden">
|
||||||
@if(listing){
|
@if(listing){
|
||||||
<div class="p-6 relative">
|
<div class="p-6 relative">
|
||||||
<h1 class="text-3xl font-bold mb-4">{{ listing?.title }}</h1>
|
<h1 class="text-3xl font-bold mb-4 break-words">{{ listing?.title }}</h1>
|
||||||
<button (click)="historyService.goBack()"
|
<button (click)="historyService.goBack()"
|
||||||
class="print:hidden absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
|
class="print:hidden absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-col lg:flex-row">
|
<div class="flex flex-col lg:flex-row">
|
||||||
<div class="w-full lg:w-1/2 pr-0 lg:pr-4">
|
<div class="w-full lg:w-1/2 pr-0 lg:pr-4">
|
||||||
<p class="mb-4" [innerHTML]="description"></p>
|
<p class="mb-4 break-words" [innerHTML]="description"></p>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div *ngFor="let detail of propertyDetails; let i = index" class="flex flex-col sm:flex-row"
|
<div *ngFor="let detail of propertyDetails; let i = index" class="flex flex-col sm:flex-row"
|
||||||
|
|
@ -20,10 +20,11 @@
|
||||||
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
|
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
|
||||||
|
|
||||||
<!-- Standard Text -->
|
<!-- Standard Text -->
|
||||||
<div class="w-full sm:w-2/3 p-2" *ngIf="!detail.isHtml && !detail.isListingBy">{{ detail.value }}</div>
|
<div class="w-full sm:w-2/3 p-2 break-words" *ngIf="!detail.isHtml && !detail.isListingBy">{{ detail.value
|
||||||
|
}}</div>
|
||||||
|
|
||||||
<!-- HTML Content (nicht für RouterLink) -->
|
<!-- HTML Content (nicht für RouterLink) -->
|
||||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value"
|
<div class="w-full sm:w-2/3 p-2 flex space-x-2 break-words" [innerHTML]="detail.value"
|
||||||
*ngIf="detail.isHtml && !detail.isListingBy"></div>
|
*ngIf="detail.isHtml && !detail.isListingBy"></div>
|
||||||
|
|
||||||
<!-- Speziell für Listing By mit RouterLink -->
|
<!-- Speziell für Listing By mit RouterLink -->
|
||||||
|
|
@ -33,7 +34,8 @@
|
||||||
detail.user.lastname }} </a>
|
detail.user.lastname }} </a>
|
||||||
<img *ngIf="detail.user.hasCompanyLogo"
|
<img *ngIf="detail.user.hasCompanyLogo"
|
||||||
[ngSrc]="detail.imageBaseUrl + '/pictures/logo/' + detail.imagePath + '.avif?_ts=' + detail.ts"
|
[ngSrc]="detail.imageBaseUrl + '/pictures/logo/' + detail.imagePath + '.avif?_ts=' + detail.ts"
|
||||||
class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" width="100" height="30" alt="Company logo for {{ detail.user.firstname }} {{ detail.user.lastname }}" />
|
class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" width="100" height="30"
|
||||||
|
alt="Company logo for {{ detail.user.firstname }} {{ detail.user.lastname }}" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -49,7 +51,7 @@
|
||||||
} @if(user){
|
} @if(user){
|
||||||
<div class="inline">
|
<div class="inline">
|
||||||
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||||
(click)="save()" [disabled]="listing.favoritesForUser.includes(user.email)">
|
(click)="toggleFavorite()">
|
||||||
<i class="fa-regular fa-heart"></i>
|
<i class="fa-regular fa-heart"></i>
|
||||||
@if(listing.favoritesForUser.includes(user.email)){
|
@if(listing.favoritesForUser.includes(user.email)){
|
||||||
<span class="ml-2">Saved ...</span>
|
<span class="ml-2">Saved ...</span>
|
||||||
|
|
@ -156,10 +158,13 @@
|
||||||
<h2 class="text-2xl font-bold mb-6 text-gray-900">Frequently Asked Questions</h2>
|
<h2 class="text-2xl font-bold mb-6 text-gray-900">Frequently Asked Questions</h2>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@for (faq of propertyFAQs; track $index) {
|
@for (faq of propertyFAQs; track $index) {
|
||||||
<details class="group border border-gray-200 rounded-lg overflow-hidden hover:border-primary-300 transition-colors">
|
<details
|
||||||
<summary class="flex items-center justify-between cursor-pointer p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
|
class="group border border-gray-200 rounded-lg overflow-hidden hover:border-primary-300 transition-colors">
|
||||||
|
<summary
|
||||||
|
class="flex items-center justify-between cursor-pointer p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
|
||||||
<h3 class="text-lg font-semibold text-gray-900">{{ faq.question }}</h3>
|
<h3 class="text-lg font-semibold text-gray-900">{{ faq.question }}</h3>
|
||||||
<svg class="w-5 h-5 text-gray-600 group-open:rotate-180 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 text-gray-600 group-open:rotate-180 transition-transform" fill="none"
|
||||||
|
stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
@ -171,7 +176,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 p-4 bg-primary-50 border-l-4 border-primary-500 rounded">
|
<div class="mt-6 p-4 bg-primary-50 border-l-4 border-primary-500 rounded">
|
||||||
<p class="text-sm text-gray-700">
|
<p class="text-sm text-gray-700">
|
||||||
<strong class="text-primary-700">Have more questions?</strong> Contact the seller directly using the form above or reach out to our support team for assistance.
|
<strong class="text-primary-700">Have more questions?</strong> Contact the seller directly using the form
|
||||||
|
above or reach out to our support team for assistance.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, NgZone } from '@angular/core';
|
import { ChangeDetectorRef, Component, NgZone } from '@angular/core';
|
||||||
import { NgOptimizedImage } from '@angular/common';
|
import { NgOptimizedImage } from '@angular/common';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
@ -92,6 +92,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||||
private emailService: EMailService,
|
private emailService: EMailService,
|
||||||
public authService: AuthService,
|
public authService: AuthService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
|
private cdref: ChangeDetectorRef,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.mailinfo = { sender: { name: '', email: '', phoneNumber: '', state: '', comments: '' }, email: '', url: environment.mailinfoUrl };
|
this.mailinfo = { sender: { name: '', email: '', phoneNumber: '', state: '', comments: '' }, email: '', url: environment.mailinfoUrl };
|
||||||
|
|
@ -314,13 +315,27 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||||
getImageIndices(): number[] {
|
getImageIndices(): number[] {
|
||||||
return this.listing && this.listing.imageOrder ? this.listing.imageOrder.slice(1).map((e, i) => i + 1) : [];
|
return this.listing && this.listing.imageOrder ? this.listing.imageOrder.slice(1).map((e, i) => i + 1) : [];
|
||||||
}
|
}
|
||||||
async save() {
|
async toggleFavorite() {
|
||||||
await this.listingsService.addToFavorites(this.listing.id, 'commercialProperty');
|
try {
|
||||||
this.listing.favoritesForUser.push(this.user.email);
|
const isFavorited = this.listing.favoritesForUser.includes(this.user.email);
|
||||||
this.auditService.createEvent(this.listing.id, 'favorite', this.user?.email);
|
|
||||||
}
|
if (isFavorited) {
|
||||||
isAlreadyFavorite() {
|
// Remove from favorites
|
||||||
return this.listing.favoritesForUser.includes(this.user.email);
|
await this.listingsService.removeFavorite(this.listing.id, 'commercialProperty');
|
||||||
|
this.listing.favoritesForUser = this.listing.favoritesForUser.filter(
|
||||||
|
email => email !== this.user.email
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Add to favorites
|
||||||
|
await this.listingsService.addToFavorites(this.listing.id, 'commercialProperty');
|
||||||
|
this.listing.favoritesForUser.push(this.user.email);
|
||||||
|
this.auditService.createEvent(this.listing.id, 'favorite', this.user?.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cdref.detectChanges();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling favorite:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async showShareByEMail() {
|
async showShareByEMail() {
|
||||||
const result = await this.emailService.showShareByEMail({
|
const result = await this.emailService.showShareByEMail({
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,12 @@
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<!-- <img src="https://placehold.co/80x80" alt="Profile picture of Avery Brown smiling" class="w-20 h-20 rounded-full" /> -->
|
<!-- <img src="https://placehold.co/80x80" alt="Profile picture of Avery Brown smiling" class="w-20 h-20 rounded-full" /> -->
|
||||||
@if(user.hasProfile){
|
@if(user.hasProfile){
|
||||||
<img ngSrc="{{ env.imageBaseUrl }}/pictures//profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="w-20 h-20 rounded-full object-cover" width="80" height="80" priority alt="Profile picture of {{ user.firstname }} {{ user.lastname }}" />
|
<img ngSrc="{{ env.imageBaseUrl }}/pictures//profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}"
|
||||||
|
class="w-20 h-20 rounded-full object-cover" width="80" height="80" priority
|
||||||
|
alt="Profile picture of {{ user.firstname }} {{ user.lastname }}" />
|
||||||
} @else {
|
} @else {
|
||||||
<img ngSrc="assets/images/person_placeholder.jpg" class="w-20 h-20 rounded-full" width="80" height="80" priority alt="Default profile picture" />
|
<img ngSrc="assets/images/person_placeholder.jpg" class="w-20 h-20 rounded-full" width="80" height="80" priority
|
||||||
|
alt="Default profile picture" />
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold flex items-center">
|
<h1 class="text-2xl font-bold flex items-center">
|
||||||
|
|
@ -32,20 +35,19 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@if(user.hasCompanyLogo){
|
@if(user.hasCompanyLogo){
|
||||||
<img ngSrc="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="w-11 h-14" width="44" height="56" alt="Company logo of {{ user.companyName }}" />
|
<img ngSrc="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}"
|
||||||
|
class="w-14 h-14 object-contain" width="56" height="56" alt="Company logo of {{ user.companyName }}" />
|
||||||
}
|
}
|
||||||
<!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> -->
|
<!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> -->
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button (click)="historyService.goBack()"
|
||||||
(click)="historyService.goBack()"
|
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
|
||||||
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
|
|
||||||
>
|
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p class="p-4 text-neutral-700">{{ user.description }}</p>
|
<p class="p-4 text-neutral-700 break-words">{{ user.description }}</p>
|
||||||
|
|
||||||
<!-- Like and Share Action Buttons -->
|
<!-- Like and Share Action Buttons -->
|
||||||
<div class="py-4 px-4 print:hidden">
|
<div class="py-4 px-4 print:hidden">
|
||||||
|
|
@ -58,10 +60,9 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if(keycloakUser && keycloakUser.email !== user.email){
|
|
||||||
<div class="inline">
|
<div class="inline">
|
||||||
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
<button type="button" class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||||
(click)="save()" [disabled]="isAlreadyFavorite()">
|
(click)="toggleFavorite()">
|
||||||
<i class="fa-regular fa-heart"></i>
|
<i class="fa-regular fa-heart"></i>
|
||||||
@if(isAlreadyFavorite()){
|
@if(isAlreadyFavorite()){
|
||||||
<span class="ml-2">Saved ...</span>
|
<span class="ml-2">Saved ...</span>
|
||||||
|
|
@ -70,7 +71,6 @@
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
<share-button button="print" showText="true" (click)="createEvent('print')"></share-button>
|
<share-button button="print" showText="true" (click)="createEvent('print')"></share-button>
|
||||||
|
|
||||||
<div class="inline">
|
<div class="inline">
|
||||||
|
|
@ -112,7 +112,7 @@
|
||||||
<!-- Company Profile -->
|
<!-- Company Profile -->
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h2 class="text-xl font-semibold mb-4">Company Profile</h2>
|
<h2 class="text-xl font-semibold mb-4">Company Profile</h2>
|
||||||
<p class="text-neutral-700 mb-4" [innerHTML]="companyOverview"></p>
|
<p class="text-neutral-700 mb-4 break-words" [innerHTML]="companyOverview"></p>
|
||||||
|
|
||||||
<!-- Profile Details -->
|
<!-- Profile Details -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|
@ -144,7 +144,7 @@
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<h3 class="font-semibold mb-2">Services we offer</h3>
|
<h3 class="font-semibold mb-2">Services we offer</h3>
|
||||||
<p class="text-neutral-700 mb-4" [innerHTML]="offeredServices"></p>
|
<p class="text-neutral-700 mb-4 break-words" [innerHTML]="offeredServices"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Areas Served -->
|
<!-- Areas Served -->
|
||||||
|
|
@ -152,7 +152,8 @@
|
||||||
<h3 class="font-semibold mb-2">Areas (Counties) we serve</h3>
|
<h3 class="font-semibold mb-2">Areas (Counties) we serve</h3>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@for (area of user.areasServed; track area) {
|
@for (area of user.areasServed; track area) {
|
||||||
<span class="bg-primary-100 text-primary-800 px-2 py-1 rounded-full text-sm">{{ area.county }}{{ area.county ? '-' : '' }}{{ area.state }}</span>
|
<span class="bg-primary-100 text-primary-800 px-2 py-1 rounded-full text-sm">{{ area.county }}{{ area.county ?
|
||||||
|
'-' : '' }}{{ area.state }}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -161,7 +162,8 @@
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<h3 class="font-semibold mb-2">Licensed In</h3>
|
<h3 class="font-semibold mb-2">Licensed In</h3>
|
||||||
@for (license of user.licensedIn; track license) {
|
@for (license of user.licensedIn; track license) {
|
||||||
<span class="bg-success-100 text-success-800 px-2 py-1 rounded-full text-sm">{{ license.registerNo }}-{{ license.state }}</span>
|
<span class="bg-success-100 text-success-800 px-2 py-1 rounded-full text-sm">{{ license.registerNo }}-{{
|
||||||
|
license.state }}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +176,8 @@
|
||||||
<h2 class="text-xl font-semibold mb-4">My Business Listings For Sale</h2>
|
<h2 class="text-xl font-semibold mb-4">My Business Listings For Sale</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
@for (listing of businessListings; track listing) {
|
@for (listing of businessListings; track listing) {
|
||||||
<div class="border rounded-lg p-4 hover:cursor-pointer" [routerLink]="['/business', listing.slug || listing.id]">
|
<div class="border rounded-lg p-4 hover:cursor-pointer"
|
||||||
|
[routerLink]="['/business', listing.slug || listing.id]">
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="mr-2"></i>
|
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="mr-2"></i>
|
||||||
<span class="font-medium">{{ selectOptions.getBusiness(listing.type) }}</span>
|
<span class="font-medium">{{ selectOptions.getBusiness(listing.type) }}</span>
|
||||||
|
|
@ -189,12 +192,17 @@
|
||||||
<h2 class="text-xl font-semibold mb-4">My Commercial Property Listings For Sale</h2>
|
<h2 class="text-xl font-semibold mb-4">My Commercial Property Listings For Sale</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
@for (listing of commercialPropListings; track listing) {
|
@for (listing of commercialPropListings; track listing) {
|
||||||
<div class="border rounded-lg p-4 hover:cursor-pointer" [routerLink]="['/commercial-property', listing.slug || listing.id]">
|
<div class="border rounded-lg p-4 hover:cursor-pointer"
|
||||||
|
[routerLink]="['/commercial-property', listing.slug || listing.id]">
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
@if (listing.imageOrder?.length>0){
|
@if (listing.imageOrder?.length>0){
|
||||||
<img ngSrc="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="w-12 h-12 object-cover rounded" width="48" height="48" alt="Property image for {{ listing.title }}" />
|
<img
|
||||||
|
ngSrc="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}"
|
||||||
|
class="w-12 h-12 object-cover rounded" width="48" height="48"
|
||||||
|
alt="Property image for {{ listing.title }}" />
|
||||||
} @else {
|
} @else {
|
||||||
<img ngSrc="assets/images/placeholder_properties.jpg" class="w-12 h-12 object-cover rounded" width="48" height="48" alt="Property placeholder image" />
|
<img ngSrc="assets/images/placeholder_properties.jpg" class="w-12 h-12 object-cover rounded" width="48"
|
||||||
|
height="48" alt="Property placeholder image" />
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium">{{ selectOptions.getCommercialProperty(listing.type) }}</p>
|
<p class="font-medium">{{ selectOptions.getCommercialProperty(listing.type) }}</p>
|
||||||
|
|
@ -208,4 +216,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component } from '@angular/core';
|
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||||
import { NgOptimizedImage } from '@angular/common';
|
import { NgOptimizedImage } from '@angular/common';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
@ -59,7 +59,8 @@ export class DetailsUserComponent {
|
||||||
private auditService: AuditService,
|
private auditService: AuditService,
|
||||||
private emailService: EMailService,
|
private emailService: EMailService,
|
||||||
private messageService: MessageService,
|
private messageService: MessageService,
|
||||||
) {}
|
private cdref: ChangeDetectorRef,
|
||||||
|
) { }
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.user = await this.userService.getById(this.id);
|
this.user = await this.userService.getById(this.id);
|
||||||
|
|
@ -74,25 +75,40 @@ export class DetailsUserComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add professional to favorites
|
* Toggle professional favorite status
|
||||||
*/
|
*/
|
||||||
async save() {
|
async toggleFavorite() {
|
||||||
await this.listingsService.addToFavorites(this.user.id, 'user');
|
try {
|
||||||
if (!this.user.favoritesForUser) {
|
const isFavorited = this.user.favoritesForUser?.includes(this.keycloakUser.email);
|
||||||
this.user.favoritesForUser = [];
|
|
||||||
|
if (isFavorited) {
|
||||||
|
// Remove from favorites
|
||||||
|
await this.listingsService.removeFavorite(this.user.id, 'user');
|
||||||
|
this.user.favoritesForUser = this.user.favoritesForUser.filter(
|
||||||
|
email => email !== this.keycloakUser.email
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Add to favorites
|
||||||
|
await this.listingsService.addToFavorites(this.user.id, 'user');
|
||||||
|
if (!this.user.favoritesForUser) {
|
||||||
|
this.user.favoritesForUser = [];
|
||||||
|
}
|
||||||
|
this.user.favoritesForUser.push(this.keycloakUser.email);
|
||||||
|
this.auditService.createEvent(this.user.id, 'favorite', this.keycloakUser?.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cdref.detectChanges();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling favorite', error);
|
||||||
}
|
}
|
||||||
this.user.favoritesForUser.push(this.keycloakUser.email);
|
|
||||||
this.auditService.createEvent(this.user.id, 'favorite', this.keycloakUser?.email);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if already in favorites
|
|
||||||
*/
|
|
||||||
isAlreadyFavorite(): boolean {
|
isAlreadyFavorite(): boolean {
|
||||||
if (!this.keycloakUser?.email || !this.user?.favoritesForUser) return false;
|
if (!this.keycloakUser?.email || !this.user?.favoritesForUser) return false;
|
||||||
return this.user.favoritesForUser.includes(this.keycloakUser.email);
|
return this.user.favoritesForUser.includes(this.keycloakUser.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show email sharing modal
|
* Show email sharing modal
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,15 @@
|
||||||
<div
|
<div
|
||||||
class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6 flex flex-col justify-between hover:shadow-2xl transition-all duration-300 hover:scale-[1.02] group relative">
|
class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6 flex flex-col justify-between hover:shadow-2xl transition-all duration-300 hover:scale-[1.02] group relative">
|
||||||
<!-- Quick Actions Overlay -->
|
<!-- Quick Actions Overlay -->
|
||||||
<div class="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-20">
|
<div
|
||||||
|
class="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-20">
|
||||||
@if(currentUser) {
|
@if(currentUser) {
|
||||||
<button class="bg-white rounded-full p-2 shadow-lg transition-colors"
|
<button type="button" class="bg-white rounded-full p-2 shadow-lg transition-colors"
|
||||||
[class.bg-red-50]="isFavorite(user)"
|
[class.bg-red-50]="isFavorite(user)"
|
||||||
[title]="isFavorite(user) ? 'Remove from favorites' : 'Save to favorites'"
|
[title]="isFavorite(user) ? 'Remove from favorites' : 'Save to favorites'"
|
||||||
(click)="toggleFavorite($event, user)">
|
(click)="toggleFavorite($event, user)">
|
||||||
<i [class]="isFavorite(user) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i>
|
<i
|
||||||
|
[class]="isFavorite(user) ? 'fas fa-heart text-red-500' : 'far fa-heart text-red-500 hover:scale-110 transition-transform'"></i>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button type="button" class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors"
|
<button type="button" class="bg-white rounded-full p-2 shadow-lg hover:bg-blue-50 transition-colors"
|
||||||
|
|
@ -134,7 +136,8 @@
|
||||||
<h2 class="text-center text-black text-xl font-semibold leading-loose pb-2">There're no professionals here
|
<h2 class="text-center text-black text-xl font-semibold leading-loose pb-2">There're no professionals here
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-center text-black text-base font-normal leading-relaxed pb-4">Try changing your filters to
|
<p class="text-center text-black text-base font-normal leading-relaxed pb-4">Try changing your filters to
|
||||||
<br />see professionals</p>
|
<br />see professionals
|
||||||
|
</p>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button (click)="clearAllFilters()"
|
<button (click)="clearAllFilters()"
|
||||||
class="w-full px-3 py-2 rounded-full border border-neutral-300 text-neutral-900 text-xs font-semibold leading-4">Clear
|
class="w-full px-3 py-2 rounded-full border border-neutral-300 text-neutral-900 text-xs font-semibold leading-4">Clear
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,9 @@ export class BrokerListingsComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Toggling favorite for:', professional.email, 'Current user:', this.currentUser.email);
|
||||||
|
console.log('Before update, favorites:', professional.favoritesForUser);
|
||||||
|
|
||||||
if (this.isFavorite(professional)) {
|
if (this.isFavorite(professional)) {
|
||||||
// Remove from favorites
|
// Remove from favorites
|
||||||
await this.listingsService.removeFavorite(professional.id, 'user');
|
await this.listingsService.removeFavorite(professional.id, 'user');
|
||||||
|
|
@ -186,8 +189,12 @@ export class BrokerListingsComponent implements OnInit, OnDestroy {
|
||||||
if (!professional.favoritesForUser) {
|
if (!professional.favoritesForUser) {
|
||||||
professional.favoritesForUser = [];
|
professional.favoritesForUser = [];
|
||||||
}
|
}
|
||||||
professional.favoritesForUser.push(this.currentUser.email);
|
// Use spread to create new array reference
|
||||||
|
professional.favoritesForUser = [...professional.favoritesForUser, this.currentUser.email];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('After update, favorites:', professional.favoritesForUser);
|
||||||
|
this.cdRef.markForCheck();
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling favorite:', error);
|
console.error('Error toggling favorite:', error);
|
||||||
|
|
|
||||||
|
|
@ -16,27 +16,52 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for(listing of favorites; track listing){
|
@for(listing of favorites; track listing){
|
||||||
|
@if(isBusinessOrCommercial(listing)){
|
||||||
<tr class="border-b">
|
<tr class="border-b">
|
||||||
<td class="py-2 px-4">{{ listing.title }}</td>
|
<td class="py-2 px-4">{{ $any(listing).title }}</td>
|
||||||
<td class="py-2 px-4">{{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</td>
|
<td class="py-2 px-4">{{ $any(listing).listingsCategory === 'commercialProperty' ? 'Commercial Property' :
|
||||||
<td class="py-2 px-4">{{ listing.location.name ? listing.location.name : listing.location.county }}, {{ listing.location.state }}</td>
|
'Business' }}</td>
|
||||||
<td class="py-2 px-4">${{ listing.price.toLocaleString() }}</td>
|
<td class="py-2 px-4">{{ listing.location.name ? listing.location.name : listing.location.county }}, {{
|
||||||
|
listing.location.state }}</td>
|
||||||
|
<td class="py-2 px-4">${{ $any(listing).price.toLocaleString() }}</td>
|
||||||
<td class="py-2 px-4 flex">
|
<td class="py-2 px-4 flex">
|
||||||
@if(listing.listingsCategory==='business'){
|
@if($any(listing).listingsCategory==='business'){
|
||||||
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" [routerLink]="['/business', listing.slug || listing.id]">
|
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
[routerLink]="['/business', $any(listing).slug || listing.id]">
|
||||||
<i class="fa-regular fa-eye"></i>
|
<i class="fa-regular fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
} @if(listing.listingsCategory==='commercialProperty'){
|
} @if($any(listing).listingsCategory==='commercialProperty'){
|
||||||
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" [routerLink]="['/commercial-property', listing.slug || listing.id]">
|
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
[routerLink]="['/commercial-property', $any(listing).slug || listing.id]">
|
||||||
<i class="fa-regular fa-eye"></i>
|
<i class="fa-regular fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button class="bg-red-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" (click)="confirmDelete(listing)">
|
<button class="bg-red-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
(click)="confirmDelete(listing)">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @else {
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="py-2 px-4">{{ $any(listing).firstname }} {{ $any(listing).lastname }}</td>
|
||||||
|
<td class="py-2 px-4">Professional</td>
|
||||||
|
<td class="py-2 px-4">{{ listing.location?.name ? listing.location.name : listing.location?.county
|
||||||
|
}}, {{ listing.location?.state }}</td>
|
||||||
|
<td class="py-2 px-4">-</td>
|
||||||
|
<td class="py-2 px-4 flex">
|
||||||
|
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
[routerLink]="['/details-user', listing.id]">
|
||||||
|
<i class="fa-regular fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
<button class="bg-red-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
(click)="confirmDelete(listing)">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -45,25 +70,47 @@
|
||||||
<!-- Mobile view -->
|
<!-- Mobile view -->
|
||||||
<div class="md:hidden">
|
<div class="md:hidden">
|
||||||
<div *ngFor="let listing of favorites" class="bg-white drop-shadow-inner-faint rounded-lg p-4 mb-4">
|
<div *ngFor="let listing of favorites" class="bg-white drop-shadow-inner-faint rounded-lg p-4 mb-4">
|
||||||
<h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2>
|
@if(isBusinessOrCommercial(listing)){
|
||||||
<p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</p>
|
<h2 class="text-xl font-semibold mb-2">{{ $any(listing).title }}</h2>
|
||||||
<p class="text-gray-600 mb-2">Located in: {{ listing.location.name ? listing.location.name : listing.location.county }}, {{ listing.location.state }}</p>
|
<p class="text-gray-600 mb-2">Category: {{ $any(listing).listingsCategory === 'commercialProperty' ? 'Commercial
|
||||||
<p class="text-gray-600 mb-2">Price: ${{ listing.price.toLocaleString() }}</p>
|
Property' : 'Business' }}</p>
|
||||||
|
<p class="text-gray-600 mb-2">Located in: {{ listing.location.name ? listing.location.name :
|
||||||
|
listing.location.county }}, {{ listing.location.state }}</p>
|
||||||
|
<p class="text-gray-600 mb-2">Price: ${{ $any(listing).price.toLocaleString() }}</p>
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
@if(listing.listingsCategory==='business'){
|
@if($any(listing).listingsCategory==='business'){
|
||||||
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" [routerLink]="['/business', listing.slug || listing.id]">
|
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
[routerLink]="['/business', $any(listing).slug || listing.id]">
|
||||||
<i class="fa-regular fa-eye"></i>
|
<i class="fa-regular fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
} @if(listing.listingsCategory==='commercialProperty'){
|
} @if($any(listing).listingsCategory==='commercialProperty'){
|
||||||
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" [routerLink]="['/commercial-property', listing.slug || listing.id]">
|
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
[routerLink]="['/commercial-property', $any(listing).slug || listing.id]">
|
||||||
<i class="fa-regular fa-eye"></i>
|
<i class="fa-regular fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button class="bg-red-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2" (click)="confirmDelete(listing)">
|
<button class="bg-red-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
(click)="confirmDelete(listing)">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
} @else {
|
||||||
|
<h2 class="text-xl font-semibold mb-2">{{ $any(listing).firstname }} {{ $any(listing).lastname }}</h2>
|
||||||
|
<p class="text-gray-600 mb-2">Category: Professional</p>
|
||||||
|
<p class="text-gray-600 mb-2">Located in: {{ listing.location?.name ? listing.location.name :
|
||||||
|
listing.location?.county }}, {{ listing.location?.state }}</p>
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<button class="bg-green-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
[routerLink]="['/details-user', listing.id]">
|
||||||
|
<i class="fa-regular fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
<button class="bg-red-500 text-white w-10 h-10 flex items-center justify-center rounded-full mr-2"
|
||||||
|
(click)="confirmDelete(listing)">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -82,4 +129,4 @@
|
||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-confirmation></app-confirmation>
|
<app-confirmation></app-confirmation>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { BusinessListing, CommercialPropertyListing } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { KeycloakUser } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { KeycloakUser } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
||||||
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
||||||
|
|
@ -19,28 +19,36 @@ import { map2User } from '../../../utils/utils';
|
||||||
export class FavoritesComponent {
|
export class FavoritesComponent {
|
||||||
user: KeycloakUser;
|
user: KeycloakUser;
|
||||||
// listings: Array<ListingType> = []; //= dataListings as unknown as Array<BusinessListing>;
|
// listings: Array<ListingType> = []; //= dataListings as unknown as Array<BusinessListing>;
|
||||||
favorites: Array<BusinessListing | CommercialPropertyListing>;
|
favorites: Array<BusinessListing | CommercialPropertyListing | User>;
|
||||||
constructor(private listingsService: ListingsService, public selectOptions: SelectOptionsService, private confirmationService: ConfirmationService, private authService: AuthService) {}
|
constructor(private listingsService: ListingsService, public selectOptions: SelectOptionsService, private confirmationService: ConfirmationService, private authService: AuthService) { }
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const token = await this.authService.getToken();
|
const token = await this.authService.getToken();
|
||||||
this.user = map2User(token);
|
this.user = map2User(token);
|
||||||
const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty')]);
|
const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty'), await this.listingsService.getFavoriteListings('user')]);
|
||||||
this.favorites = [...result[0], ...result[1]];
|
this.favorites = [...result[0], ...result[1], ...result[2]] as Array<BusinessListing | CommercialPropertyListing | User>;
|
||||||
}
|
}
|
||||||
async confirmDelete(listing: BusinessListing | CommercialPropertyListing) {
|
async confirmDelete(listing: BusinessListing | CommercialPropertyListing | User) {
|
||||||
const confirmed = await this.confirmationService.showConfirmation({ message: `Are you sure you want to remove this listing from your Favorites?` });
|
const confirmed = await this.confirmationService.showConfirmation({ message: `Are you sure you want to remove this listing from your Favorites?` });
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
// this.messageService.showMessage('Listing has been deleted');
|
// this.messageService.showMessage('Listing has been deleted');
|
||||||
this.deleteListing(listing);
|
this.deleteListing(listing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async deleteListing(listing: BusinessListing | CommercialPropertyListing) {
|
async deleteListing(listing: BusinessListing | CommercialPropertyListing | User) {
|
||||||
if (listing.listingsCategory === 'business') {
|
if ('listingsCategory' in listing) {
|
||||||
await this.listingsService.removeFavorite(listing.id, 'business');
|
if (listing.listingsCategory === 'business') {
|
||||||
|
await this.listingsService.removeFavorite(listing.id, 'business');
|
||||||
|
} else {
|
||||||
|
await this.listingsService.removeFavorite(listing.id, 'commercialProperty');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.listingsService.removeFavorite(listing.id, 'commercialProperty');
|
await this.listingsService.removeFavorite(listing.id, 'user');
|
||||||
}
|
}
|
||||||
const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty')]);
|
const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty'), await this.listingsService.getFavoriteListings('user')]);
|
||||||
this.favorites = [...result[0], ...result[1]];
|
this.favorites = [...result[0], ...result[1], ...result[2]] as Array<BusinessListing | CommercialPropertyListing | User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
isBusinessOrCommercial(listing: any): boolean {
|
||||||
|
return !!listing.listingsCategory;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { getCriteriaByListingCategory, getSortByListingCategory } from '../utils
|
||||||
})
|
})
|
||||||
export class ListingsService {
|
export class ListingsService {
|
||||||
private apiBaseUrl = environment.apiBaseUrl;
|
private apiBaseUrl = environment.apiBaseUrl;
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
async getListings(listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty'): Promise<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray> {
|
async getListings(listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty'): Promise<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray> {
|
||||||
const criteria = getCriteriaByListingCategory(listingsCategory);
|
const criteria = getCriteriaByListingCategory(listingsCategory);
|
||||||
|
|
@ -35,8 +35,8 @@ export class ListingsService {
|
||||||
getListingsByEmail(email: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
|
getListingsByEmail(email: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
|
||||||
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${email}`));
|
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${email}`));
|
||||||
}
|
}
|
||||||
getFavoriteListings(listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
|
getFavoriteListings(listingsCategory: 'business' | 'commercialProperty' | 'user'): Promise<ListingType[]> {
|
||||||
return lastValueFrom(this.http.get<BusinessListing[] | CommercialPropertyListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/favorites/all`));
|
return lastValueFrom(this.http.post<BusinessListing[] | CommercialPropertyListing[] | any[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/favorites/all`, {}));
|
||||||
}
|
}
|
||||||
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
|
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
|
||||||
if (listing.id) {
|
if (listing.id) {
|
||||||
|
|
@ -52,10 +52,14 @@ export class ListingsService {
|
||||||
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/listing/${id}/${imagePath}`));
|
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/listing/${id}/${imagePath}`));
|
||||||
}
|
}
|
||||||
async addToFavorites(id: string, listingsCategory?: 'business' | 'commercialProperty' | 'user') {
|
async addToFavorites(id: string, listingsCategory?: 'business' | 'commercialProperty' | 'user') {
|
||||||
await lastValueFrom(this.http.post<void>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/favorite/${id}`, {}));
|
const url = `${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/favorite/${id}`;
|
||||||
|
console.log('[ListingsService] addToFavorites calling URL:', url);
|
||||||
|
await lastValueFrom(this.http.post<void>(url, {}));
|
||||||
}
|
}
|
||||||
async removeFavorite(id: string, listingsCategory?: 'business' | 'commercialProperty' | 'user') {
|
async removeFavorite(id: string, listingsCategory?: 'business' | 'commercialProperty' | 'user') {
|
||||||
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/favorite/${id}`));
|
const url = `${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/favorite/${id}`;
|
||||||
|
console.log('[ListingsService] removeFavorite calling URL:', url);
|
||||||
|
await lastValueFrom(this.http.delete<ListingType>(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue