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();
|
||||
|
|
@ -15,17 +15,19 @@ export class BusinessListingsController {
|
|||
@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)
|
||||
@Get(':slugOrId')
|
||||
async findById(@Request() req, @Param('slugOrId') slugOrId: string): Promise<any> {
|
||||
// Support both slug (e.g., "restaurant-austin-tx-a3f7b2c1") and UUID
|
||||
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)
|
||||
@Get('user/:userid')
|
||||
async findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ export class CommercialPropertyListingsController {
|
|||
@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)
|
||||
@Get(':slugOrId')
|
||||
async findById(@Request() req, @Param('slugOrId') slugOrId: string): Promise<any> {
|
||||
|
|
@ -24,12 +30,6 @@ export class CommercialPropertyListingsController {
|
|||
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)
|
||||
@Get('user/:email')
|
||||
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 { BusinessListingsController } from './business-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 { GeoModule } from '../geo/geo.module';
|
||||
|
|
@ -16,7 +17,7 @@ import { UnknownListingsController } from './unknown-listings.controller';
|
|||
|
||||
@Module({
|
||||
imports: [DrizzleModule, AuthModule, GeoModule,FirebaseAdminModule],
|
||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController, UserListingsController],
|
||||
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
|
||||
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);
|
||||
|
|
@ -158,4 +158,34 @@ export class UserService {
|
|||
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">
|
||||
<!-- Left column -->
|
||||
<div class="w-full lg:w-1/2 pr-0 lg:pr-6">
|
||||
<h1 class="text-2xl font-bold mb-4">{{ listing.title }}</h1>
|
||||
<p class="mb-4" [innerHTML]="description"></p>
|
||||
<h1 class="text-2xl font-bold mb-4 break-words">{{ listing.title }}</h1>
|
||||
<p class="mb-4 break-words" [innerHTML]="description"></p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div *ngFor="let detail of listingDetails; let i = index" class="flex flex-col sm:flex-row"
|
||||
[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-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>
|
||||
|
||||
<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>
|
||||
<img *ngIf="listing.imageName"
|
||||
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>
|
||||
|
|
@ -48,7 +50,7 @@
|
|||
} @if(user){
|
||||
<div class="inline">
|
||||
<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>
|
||||
@if(listing.favoritesForUser.includes(user.email)){
|
||||
<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>
|
||||
<div class="space-y-4">
|
||||
@for (faq of businessFAQs; track $index) {
|
||||
<details 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">
|
||||
<details
|
||||
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>
|
||||
<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>
|
||||
</svg>
|
||||
</summary>
|
||||
|
|
@ -157,7 +162,8 @@
|
|||
</div>
|
||||
<div class="mt-6 p-4 bg-primary-50 border-l-4 border-primary-500 rounded">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -379,13 +379,27 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
async save() {
|
||||
async toggleFavorite() {
|
||||
try {
|
||||
const isFavorited = this.listing.favoritesForUser.includes(this.user.email);
|
||||
|
||||
if (isFavorited) {
|
||||
// Remove from favorites
|
||||
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);
|
||||
}
|
||||
isAlreadyFavorite() {
|
||||
return this.listing.favoritesForUser.includes(this.user.email);
|
||||
|
||||
this.cdref.detectChanges();
|
||||
} catch (error) {
|
||||
console.error('Error toggling favorite:', error);
|
||||
}
|
||||
}
|
||||
async 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">
|
||||
@if(listing){
|
||||
<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()"
|
||||
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>
|
||||
</button>
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<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 *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>
|
||||
|
||||
<!-- 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) -->
|
||||
<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>
|
||||
|
||||
<!-- Speziell für Listing By mit RouterLink -->
|
||||
|
|
@ -33,7 +34,8 @@
|
|||
detail.user.lastname }} </a>
|
||||
<img *ngIf="detail.user.hasCompanyLogo"
|
||||
[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>
|
||||
|
|
@ -49,7 +51,7 @@
|
|||
} @if(user){
|
||||
<div class="inline">
|
||||
<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>
|
||||
@if(listing.favoritesForUser.includes(user.email)){
|
||||
<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>
|
||||
<div class="space-y-4">
|
||||
@for (faq of propertyFAQs; track $index) {
|
||||
<details 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">
|
||||
<details
|
||||
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>
|
||||
<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>
|
||||
</svg>
|
||||
</summary>
|
||||
|
|
@ -171,7 +176,8 @@
|
|||
</div>
|
||||
<div class="mt-6 p-4 bg-primary-50 border-l-4 border-primary-500 rounded">
|
||||
<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>
|
||||
</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 { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
|
@ -92,6 +92,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
|||
private emailService: EMailService,
|
||||
public authService: AuthService,
|
||||
private seoService: SeoService,
|
||||
private cdref: ChangeDetectorRef,
|
||||
) {
|
||||
super();
|
||||
this.mailinfo = { sender: { name: '', email: '', phoneNumber: '', state: '', comments: '' }, email: '', url: environment.mailinfoUrl };
|
||||
|
|
@ -314,13 +315,27 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
|||
getImageIndices(): number[] {
|
||||
return this.listing && this.listing.imageOrder ? this.listing.imageOrder.slice(1).map((e, i) => i + 1) : [];
|
||||
}
|
||||
async save() {
|
||||
async toggleFavorite() {
|
||||
try {
|
||||
const isFavorited = this.listing.favoritesForUser.includes(this.user.email);
|
||||
|
||||
if (isFavorited) {
|
||||
// Remove from favorites
|
||||
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);
|
||||
}
|
||||
isAlreadyFavorite() {
|
||||
return this.listing.favoritesForUser.includes(this.user.email);
|
||||
|
||||
this.cdref.detectChanges();
|
||||
} catch (error) {
|
||||
console.error('Error toggling favorite:', error);
|
||||
}
|
||||
}
|
||||
async showShareByEMail() {
|
||||
const result = await this.emailService.showShareByEMail({
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@
|
|||
<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" /> -->
|
||||
@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 {
|
||||
<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>
|
||||
<h1 class="text-2xl font-bold flex items-center">
|
||||
|
|
@ -32,20 +35,19 @@
|
|||
</p>
|
||||
</div>
|
||||
@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" /> -->
|
||||
</div>
|
||||
<button
|
||||
(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"
|
||||
>
|
||||
<button (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">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="py-4 px-4 print:hidden">
|
||||
|
|
@ -58,10 +60,9 @@
|
|||
</button>
|
||||
</div>
|
||||
}
|
||||
@if(keycloakUser && keycloakUser.email !== user.email){
|
||||
<div class="inline">
|
||||
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="save()" [disabled]="isAlreadyFavorite()">
|
||||
<button type="button" class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"
|
||||
(click)="toggleFavorite()">
|
||||
<i class="fa-regular fa-heart"></i>
|
||||
@if(isAlreadyFavorite()){
|
||||
<span class="ml-2">Saved ...</span>
|
||||
|
|
@ -70,7 +71,6 @@
|
|||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<share-button button="print" showText="true" (click)="createEvent('print')"></share-button>
|
||||
|
||||
<div class="inline">
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
<!-- Company Profile -->
|
||||
<div class="p-4">
|
||||
<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 -->
|
||||
<div class="space-y-2">
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
<!-- Services -->
|
||||
<div class="mt-6">
|
||||
<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>
|
||||
|
||||
<!-- Areas Served -->
|
||||
|
|
@ -152,7 +152,8 @@
|
|||
<h3 class="font-semibold mb-2">Areas (Counties) we serve</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@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>
|
||||
|
|
@ -161,7 +162,8 @@
|
|||
<div class="mt-6">
|
||||
<h3 class="font-semibold mb-2">Licensed In</h3>
|
||||
@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>
|
||||
}
|
||||
|
|
@ -174,7 +176,8 @@
|
|||
<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">
|
||||
@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">
|
||||
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="mr-2"></i>
|
||||
<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>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@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">
|
||||
@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 {
|
||||
<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>
|
||||
<p class="font-medium">{{ selectOptions.getCommercialProperty(listing.type) }}</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { NgOptimizedImage } from '@angular/common';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
|
@ -59,6 +59,7 @@ export class DetailsUserComponent {
|
|||
private auditService: AuditService,
|
||||
private emailService: EMailService,
|
||||
private messageService: MessageService,
|
||||
private cdref: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
async ngOnInit() {
|
||||
|
|
@ -74,9 +75,20 @@ export class DetailsUserComponent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Add professional to favorites
|
||||
* Toggle professional favorite status
|
||||
*/
|
||||
async save() {
|
||||
async toggleFavorite() {
|
||||
try {
|
||||
const isFavorited = this.user.favoritesForUser?.includes(this.keycloakUser.email);
|
||||
|
||||
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 = [];
|
||||
|
|
@ -85,14 +97,18 @@ export class DetailsUserComponent {
|
|||
this.auditService.createEvent(this.user.id, 'favorite', this.keycloakUser?.email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if already in favorites
|
||||
*/
|
||||
this.cdref.detectChanges();
|
||||
} catch (error) {
|
||||
console.error('Error toggling favorite', error);
|
||||
}
|
||||
}
|
||||
|
||||
isAlreadyFavorite(): boolean {
|
||||
if (!this.keycloakUser?.email || !this.user?.favoritesForUser) return false;
|
||||
return this.user.favoritesForUser.includes(this.keycloakUser.email);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show email sharing modal
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -36,13 +36,15 @@
|
|||
<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">
|
||||
<!-- 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) {
|
||||
<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)"
|
||||
[title]="isFavorite(user) ? 'Remove from favorites' : 'Save to favorites'"
|
||||
(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 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>
|
||||
<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">
|
||||
<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
|
||||
|
|
|
|||
|
|
@ -174,6 +174,9 @@ export class BrokerListingsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
try {
|
||||
console.log('Toggling favorite for:', professional.email, 'Current user:', this.currentUser.email);
|
||||
console.log('Before update, favorites:', professional.favoritesForUser);
|
||||
|
||||
if (this.isFavorite(professional)) {
|
||||
// Remove from favorites
|
||||
await this.listingsService.removeFavorite(professional.id, 'user');
|
||||
|
|
@ -186,8 +189,12 @@ export class BrokerListingsComponent implements OnInit, OnDestroy {
|
|||
if (!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();
|
||||
} catch (error) {
|
||||
console.error('Error toggling favorite:', error);
|
||||
|
|
|
|||
|
|
@ -16,27 +16,52 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
@for(listing of favorites; track listing){
|
||||
@if(isBusinessOrCommercial(listing)){
|
||||
<tr class="border-b">
|
||||
<td class="py-2 px-4">{{ listing.title }}</td>
|
||||
<td class="py-2 px-4">{{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</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">${{ listing.price.toLocaleString() }}</td>
|
||||
<td class="py-2 px-4">{{ $any(listing).title }}</td>
|
||||
<td class="py-2 px-4">{{ $any(listing).listingsCategory === 'commercialProperty' ? 'Commercial Property' :
|
||||
'Business' }}</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">
|
||||
@if(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]">
|
||||
@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', $any(listing).slug || listing.id]">
|
||||
<i class="fa-regular fa-eye"></i>
|
||||
</button>
|
||||
|
||||
} @if(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]">
|
||||
} @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', $any(listing).slug || 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)">
|
||||
<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>
|
||||
} @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>
|
||||
</table>
|
||||
|
|
@ -45,25 +70,47 @@
|
|||
<!-- Mobile view -->
|
||||
<div class="md:hidden">
|
||||
<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>
|
||||
<p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial 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: ${{ listing.price.toLocaleString() }}</p>
|
||||
@if(isBusinessOrCommercial(listing)){
|
||||
<h2 class="text-xl font-semibold mb-2">{{ $any(listing).title }}</h2>
|
||||
<p class="text-gray-600 mb-2">Category: {{ $any(listing).listingsCategory === 'commercialProperty' ? 'Commercial
|
||||
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">
|
||||
@if(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]">
|
||||
@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', $any(listing).slug || listing.id]">
|
||||
<i class="fa-regular fa-eye"></i>
|
||||
</button>
|
||||
|
||||
} @if(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]">
|
||||
} @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', $any(listing).slug || 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)">
|
||||
<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>
|
||||
} @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>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
||||
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
||||
|
|
@ -19,28 +19,36 @@ import { map2User } from '../../../utils/utils';
|
|||
export class FavoritesComponent {
|
||||
user: KeycloakUser;
|
||||
// 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) { }
|
||||
async ngOnInit() {
|
||||
const token = await this.authService.getToken();
|
||||
this.user = map2User(token);
|
||||
const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty')]);
|
||||
this.favorites = [...result[0], ...result[1]];
|
||||
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], ...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?` });
|
||||
if (confirmed) {
|
||||
// this.messageService.showMessage('Listing has been deleted');
|
||||
this.deleteListing(listing);
|
||||
}
|
||||
}
|
||||
async deleteListing(listing: BusinessListing | CommercialPropertyListing) {
|
||||
async deleteListing(listing: BusinessListing | CommercialPropertyListing | User) {
|
||||
if ('listingsCategory' in listing) {
|
||||
if (listing.listingsCategory === 'business') {
|
||||
await this.listingsService.removeFavorite(listing.id, 'business');
|
||||
} else {
|
||||
await this.listingsService.removeFavorite(listing.id, 'commercialProperty');
|
||||
}
|
||||
const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty')]);
|
||||
this.favorites = [...result[0], ...result[1]];
|
||||
} else {
|
||||
await this.listingsService.removeFavorite(listing.id, 'user');
|
||||
}
|
||||
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], ...result[2]] as Array<BusinessListing | CommercialPropertyListing | User>;
|
||||
}
|
||||
|
||||
isBusinessOrCommercial(listing: any): boolean {
|
||||
return !!listing.listingsCategory;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ export class ListingsService {
|
|||
getListingsByEmail(email: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
|
||||
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${email}`));
|
||||
}
|
||||
getFavoriteListings(listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
|
||||
return lastValueFrom(this.http.get<BusinessListing[] | CommercialPropertyListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/favorites/all`));
|
||||
getFavoriteListings(listingsCategory: 'business' | 'commercialProperty' | 'user'): Promise<ListingType[]> {
|
||||
return lastValueFrom(this.http.post<BusinessListing[] | CommercialPropertyListing[] | any[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/favorites/all`, {}));
|
||||
}
|
||||
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
|
||||
if (listing.id) {
|
||||
|
|
@ -52,10 +52,14 @@ export class ListingsService {
|
|||
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/listing/${id}/${imagePath}`));
|
||||
}
|
||||
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') {
|
||||
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