adding filters to my-listing (listingnumber), updated/new label
This commit is contained in:
parent
d48cd7aa1d
commit
571cfb0e61
|
|
@ -152,9 +152,33 @@ export class BusinessListingService {
|
||||||
case 'creationDateLast':
|
case 'creationDateLast':
|
||||||
query.orderBy(desc(sql`${businesses_json.data}->>'created'`));
|
query.orderBy(desc(sql`${businesses_json.data}->>'created'`));
|
||||||
break;
|
break;
|
||||||
default:
|
default: {
|
||||||
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
|
// NEU (created < 14 Tage) > UPDATED (updated < 14 Tage) > Rest
|
||||||
|
const recencyRank = sql`
|
||||||
|
CASE
|
||||||
|
WHEN ((${businesses_json.data}->>'created')::timestamptz >= (now() - interval '14 days')) THEN 2
|
||||||
|
WHEN ((${businesses_json.data}->>'updated')::timestamptz >= (now() - interval '14 days')) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Innerhalb der Gruppe:
|
||||||
|
// NEW → created DESC
|
||||||
|
// UPDATED → updated DESC
|
||||||
|
// Rest → created DESC
|
||||||
|
const groupTimestamp = sql`
|
||||||
|
CASE
|
||||||
|
WHEN ((${businesses_json.data}->>'created')::timestamptz >= (now() - interval '14 days'))
|
||||||
|
THEN (${businesses_json.data}->>'created')::timestamptz
|
||||||
|
WHEN ((${businesses_json.data}->>'updated')::timestamptz >= (now() - interval '14 days'))
|
||||||
|
THEN (${businesses_json.data}->>'updated')::timestamptz
|
||||||
|
ELSE (${businesses_json.data}->>'created')::timestamptz
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
|
||||||
|
query.orderBy(desc(recencyRank), desc(groupTimestamp), desc(sql`(${businesses_json.data}->>'created')::timestamptz`));
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Paginierung
|
// Paginierung
|
||||||
query.limit(length).offset(start);
|
query.limit(length).offset(start);
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,20 @@
|
||||||
<span class="w-fit inline-flex items-center justify-center px-2 py-1 mb-4 text-xs font-bold leading-none bg-gray-200 text-gray-700 rounded-full">
|
<span class="w-fit inline-flex items-center justify-center px-2 py-1 mb-4 text-xs font-bold leading-none bg-gray-200 text-gray-700 rounded-full">
|
||||||
{{ selectOptions.getState(listing.location.state) }}
|
{{ selectOptions.getState(listing.location.state) }}
|
||||||
</span>
|
</span>
|
||||||
<p class="text-sm text-gray-600 mb-4">
|
|
||||||
<strong>{{ getDaysListed(listing) }} days listed</strong>
|
@if (getListingBadge(listing); as badge) {
|
||||||
</p>
|
<span
|
||||||
|
class="mb-4 h-fit inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none rounded-full"
|
||||||
|
[ngClass]="{
|
||||||
|
'bg-emerald-100 text-emerald-800': badge === 'NEW',
|
||||||
|
'bg-blue-100 text-blue-800': badge === 'UPDATED'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ badge }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-base font-bold text-gray-800 mb-2">
|
<p class="text-base font-bold text-gray-800 mb-2">
|
||||||
<strong>Asking price:</strong> <span class="text-green-600"> {{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}</span>
|
<strong>Asking price:</strong> <span class="text-green-600"> {{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,16 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
|
||||||
if (!listing.location) return 'Location not specified';
|
if (!listing.location) return 'Location not specified';
|
||||||
return `${listing.location.name}, ${listing.location.state}`;
|
return `${listing.location.name}, ${listing.location.state}`;
|
||||||
}
|
}
|
||||||
|
private isWithinDays(date: Date | string | undefined | null, days: number): boolean {
|
||||||
|
if (!date) return false;
|
||||||
|
return dayjs().diff(dayjs(date), 'day') < days;
|
||||||
|
}
|
||||||
|
|
||||||
|
getListingBadge(listing: BusinessListing): 'NEW' | 'UPDATED' | null {
|
||||||
|
if (this.isWithinDays(listing.created, 14)) return 'NEW'; // Priorität
|
||||||
|
if (this.isWithinDays(listing.updated, 14)) return 'UPDATED';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
navigateToDetails(listingId: string): void {
|
navigateToDetails(listingId: string): void {
|
||||||
this.router.navigate(['/details-business', listingId]);
|
this.router.navigate(['/details-business', listingId]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,85 @@
|
||||||
|
|
||||||
<!-- Desktop view -->
|
<!-- Desktop view -->
|
||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
<table class="w-full bg-white drop-shadow-inner-faint rounded-lg overflow-hidden">
|
<table class="w-full table-fixed bg-white drop-shadow-inner-faint rounded-lg overflow-hidden">
|
||||||
|
<colgroup>
|
||||||
|
<col class="w-auto" />
|
||||||
|
<!-- Title: restliche Breite -->
|
||||||
|
<col class="w-40" />
|
||||||
|
<!-- Category -->
|
||||||
|
<col class="w-60" />
|
||||||
|
<!-- Located in -->
|
||||||
|
<col class="w-32" />
|
||||||
|
<!-- Price -->
|
||||||
|
<col class="w-28" />
|
||||||
|
<!-- Internal # -->
|
||||||
|
<col class="w-40" />
|
||||||
|
<!-- Publication Status -->
|
||||||
|
<col class="w-36" />
|
||||||
|
<!-- Actions -->
|
||||||
|
</colgroup>
|
||||||
<thead class="bg-gray-100">
|
<thead class="bg-gray-100">
|
||||||
|
<!-- Header -->
|
||||||
<tr>
|
<tr>
|
||||||
<th class="py-2 px-4 text-left">Title</th>
|
<th class="py-2 px-4 text-left">Title</th>
|
||||||
<th class="py-2 px-4 text-left">Category</th>
|
<th class="py-2 px-4 text-left">Category</th>
|
||||||
<th class="py-2 px-4 text-left">Located in</th>
|
<th class="py-2 px-4 text-left">Located in</th>
|
||||||
<th class="py-2 px-4 text-left">Price</th>
|
<th class="py-2 px-4 text-left">Price</th>
|
||||||
|
<th class="py-2 px-4 text-left">Internal #</th>
|
||||||
<th class="py-2 px-4 text-left">Publication Status</th>
|
<th class="py-2 px-4 text-left">Publication Status</th>
|
||||||
<th class="py-2 px-4 text-left">Actions</th>
|
<th class="py-2 px-4 text-left whitespace-nowrap">Actions</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Filter row (zwischen Header und Inhalt) -->
|
||||||
|
<tr class="bg-white border-b">
|
||||||
|
<th class="py-2 px-4">
|
||||||
|
<input type="text" class="w-full border rounded px-2 py-1" placeholder="Filter title…" [(ngModel)]="filters.title" (input)="applyFilters()" />
|
||||||
|
</th>
|
||||||
|
<!-- Category Filter -->
|
||||||
|
<th class="py-2 px-4">
|
||||||
|
<select class="w-full border rounded px-2 py-1" [(ngModel)]="filters.category" (change)="applyFilters()">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="business">Business</option>
|
||||||
|
<option value="commercialProperty">Commercial Property</option>
|
||||||
|
</select>
|
||||||
|
</th>
|
||||||
|
<th class="py-2 px-4">
|
||||||
|
<input type="text" class="w-full border rounded px-2 py-1" placeholder="City/County/State…" [(ngModel)]="filters.location" (input)="applyFilters()" />
|
||||||
|
</th>
|
||||||
|
<th class="py-2 px-4">
|
||||||
|
<!-- Preis nicht gefiltert, daher leer -->
|
||||||
|
</th>
|
||||||
|
<th class="py-2 px-4">
|
||||||
|
<input type="text" class="w-full border rounded px-2 py-1" placeholder="Internal #" [(ngModel)]="filters.internalListingNumber" (input)="applyFilters()" />
|
||||||
|
</th>
|
||||||
|
<th class="py-2 px-4">
|
||||||
|
<select class="w-full border rounded px-2 py-1" [(ngModel)]="filters.status" (change)="applyFilters()">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="published">Published</option>
|
||||||
|
<option value="draft">Draft</option>
|
||||||
|
</select>
|
||||||
|
</th>
|
||||||
|
<th class="py-2 px-4">
|
||||||
|
<button class="text-sm underline" (click)="clearFilters()">Clear</button>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let listing of myListings" class="border-b">
|
<tr *ngFor="let listing of myListings" class="border-b">
|
||||||
<td class="py-2 px-4">{{ listing.title }}</td>
|
<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.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.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">${{ listing.price.toLocaleString() }}</td>
|
||||||
|
<td class="py-2 px-4 flex justify-center">
|
||||||
|
{{ listing.internalListingNumber ?? '—' }}
|
||||||
|
</td>
|
||||||
<td class="py-2 px-4">
|
<td class="py-2 px-4">
|
||||||
<span class="{{ listing.draft ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800' }} py-1 px-2 rounded-full text-xs font-medium">
|
<span class="{{ listing.draft ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800' }} py-1 px-2 rounded-full text-xs font-medium">
|
||||||
{{ listing.draft ? 'Draft' : 'Published' }}
|
{{ listing.draft ? 'Draft' : 'Published' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-4">
|
<td class="py-2 px-4 whitespace-nowrap">
|
||||||
@if(listing.listingsCategory==='business'){
|
@if(listing.listingsCategory==='business'){
|
||||||
<button class="bg-green-500 text-white p-2 rounded-full mr-2" [routerLink]="['/editBusinessListing', listing.id]">
|
<button class="bg-green-500 text-white p-2 rounded-full mr-2" [routerLink]="['/editBusinessListing', listing.id]">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
|
@ -57,11 +113,27 @@
|
||||||
|
|
||||||
<!-- Mobile view -->
|
<!-- Mobile view -->
|
||||||
<div class="md:hidden">
|
<div class="md:hidden">
|
||||||
|
<!-- Mobile Filter -->
|
||||||
|
<div class="bg-white drop-shadow-inner-faint rounded-lg p-4 mb-4 border">
|
||||||
|
<div class="grid grid-cols-1 gap-3">
|
||||||
|
<input type="text" class="w-full border rounded px-3 py-2" placeholder="Filter title…" [(ngModel)]="filters.title" (input)="applyFilters()" />
|
||||||
|
<input type="text" class="w-full border rounded px-3 py-2" placeholder="City/County/State…" [(ngModel)]="filters.location" (input)="applyFilters()" />
|
||||||
|
<input type="text" class="w-full border rounded px-3 py-2" placeholder="Internal #" [(ngModel)]="filters.internalListingNumber" (input)="applyFilters()" />
|
||||||
|
<select class="w-full border rounded px-3 py-2" [(ngModel)]="filters.status" (change)="applyFilters()">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="published">Published</option>
|
||||||
|
<option value="draft">Draft</option>
|
||||||
|
</select>
|
||||||
|
<button class="text-sm underline justify-self-start" (click)="clearFilters()">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div *ngFor="let listing of myListings" class="bg-white drop-shadow-inner-faint rounded-lg p-4 mb-4">
|
<div *ngFor="let listing of myListings" class="bg-white drop-shadow-inner-faint rounded-lg p-4 mb-4">
|
||||||
<h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2>
|
<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">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">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>
|
<p class="text-gray-600 mb-2">Price: ${{ listing.price.toLocaleString() }}</p>
|
||||||
|
<p class="text-gray-600 mb-2">Internal #: {{ listing.internalListingNumber ?? '—' }}</p>
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<span class="text-gray-600">Publication Status:</span>
|
<span class="text-gray-600">Publication Status:</span>
|
||||||
<span class="{{ listing.draft ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800' }} py-1 px-2 rounded-full text-xs font-medium">
|
<span class="{{ listing.draft ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800' }} py-1 px-2 rounded-full text-xs font-medium">
|
||||||
|
|
@ -96,4 +168,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-confirmation></app-confirmation>
|
<app-confirmation></app-confirmation>
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,22 @@ import { map2User } from '../../../utils/utils';
|
||||||
styleUrl: './my-listing.component.scss',
|
styleUrl: './my-listing.component.scss',
|
||||||
})
|
})
|
||||||
export class MyListingComponent {
|
export class MyListingComponent {
|
||||||
listings: Array<ListingType> = []; //dataListings as unknown as Array<BusinessListing>;
|
// Vollständige, ungefilterte Daten
|
||||||
myListings: Array<ListingType>;
|
listings: Array<ListingType> = [];
|
||||||
|
// Aktuell angezeigte (gefilterte) Daten
|
||||||
|
myListings: Array<ListingType> = [];
|
||||||
|
|
||||||
user: User;
|
user: User;
|
||||||
|
|
||||||
|
// VERY small filter state
|
||||||
|
filters = {
|
||||||
|
title: '',
|
||||||
|
internalListingNumber: '',
|
||||||
|
location: '',
|
||||||
|
status: '' as '' | 'published' | 'draft',
|
||||||
|
category: '' as '' | 'business' | 'commercialProperty', // <── NEU
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public userService: UserService,
|
public userService: UserService,
|
||||||
private listingsService: ListingsService,
|
private listingsService: ListingsService,
|
||||||
|
|
@ -33,23 +46,64 @@ export class MyListingComponent {
|
||||||
private confirmationService: ConfirmationService,
|
private confirmationService: ConfirmationService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const token = await this.authService.getToken();
|
const token = await this.authService.getToken();
|
||||||
const keycloakUser = map2User(token);
|
const keycloakUser = map2User(token);
|
||||||
const email = keycloakUser.email;
|
const email = keycloakUser.email;
|
||||||
this.user = await this.userService.getByMail(email);
|
this.user = await this.userService.getByMail(email);
|
||||||
const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
|
||||||
this.myListings = [...result[0], ...result[1]];
|
const result = await Promise.all([this.listingsService.getListingsByEmail(this.user.email, 'business'), this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
||||||
|
|
||||||
|
this.listings = [...result[0], ...result[1]];
|
||||||
|
this.myListings = this.listings;
|
||||||
|
}
|
||||||
|
private normalize(s: string | undefined | null): string {
|
||||||
|
return (s ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, ' ') // Kommas, Bindestriche etc. neutralisieren
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, ' '); // Mehrfach-Spaces zu einem Space
|
||||||
|
}
|
||||||
|
applyFilters() {
|
||||||
|
const titleQ = this.normalize(this.filters.title);
|
||||||
|
const locQ = this.normalize(this.filters.location);
|
||||||
|
const intQ = this.normalize(this.filters.internalListingNumber);
|
||||||
|
const catQ = this.filters.category; // <── NEU
|
||||||
|
const status = this.filters.status;
|
||||||
|
|
||||||
|
this.myListings = this.listings.filter(l => {
|
||||||
|
const okTitle = !titleQ || this.normalize(l.title).includes(titleQ);
|
||||||
|
|
||||||
|
const locStr = this.normalize(`${l.location?.name ? l.location.name : l.location?.county} ${l.location?.state}`);
|
||||||
|
const okLoc = !locQ || locStr.includes(locQ);
|
||||||
|
|
||||||
|
const ilnStr = this.normalize((l as any).internalListingNumber?.toString());
|
||||||
|
const okInt = !intQ || ilnStr.includes(intQ);
|
||||||
|
|
||||||
|
const okCat = !catQ || l.listingsCategory === catQ; // <── NEU
|
||||||
|
|
||||||
|
const isDraft = !!(l as any).draft;
|
||||||
|
const okStatus = !status || (status === 'published' && !isDraft) || (status === 'draft' && isDraft);
|
||||||
|
|
||||||
|
return okTitle && okLoc && okInt && okCat && okStatus; // <── NEU
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFilters() {
|
||||||
|
this.filters = { title: '', internalListingNumber: '', location: '', status: '', category: '' };
|
||||||
|
this.myListings = this.listings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteListing(listing: ListingType) {
|
async deleteListing(listing: ListingType) {
|
||||||
if (listing.listingsCategory === 'business') {
|
if (listing.listingsCategory === 'business') {
|
||||||
await this.listingsService.deleteBusinessListing(listing.id);
|
await this.listingsService.deleteBusinessListing(listing.id);
|
||||||
} else {
|
} else {
|
||||||
await this.listingsService.deleteCommercialPropertyListing(listing.id, (<CommercialPropertyListing>listing).imagePath);
|
await this.listingsService.deleteCommercialPropertyListing(listing.id, (listing as CommercialPropertyListing).imagePath);
|
||||||
}
|
}
|
||||||
const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
const result = await Promise.all([this.listingsService.getListingsByEmail(this.user.email, 'business'), this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
||||||
this.myListings = [...result[0], ...result[1]];
|
this.listings = [...result[0], ...result[1]];
|
||||||
|
this.applyFilters(); // Filter beibehalten nach Löschen
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm(listing: ListingType) {
|
async confirm(listing: ListingType) {
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 MiB |
Loading…
Reference in New Issue