adding filters to my-listing (listingnumber), updated/new label
This commit is contained in:
parent
d48cd7aa1d
commit
571cfb0e61
|
|
@ -152,10 +152,34 @@ export class BusinessListingService {
|
|||
case 'creationDateLast':
|
||||
query.orderBy(desc(sql`${businesses_json.data}->>'created'`));
|
||||
break;
|
||||
default:
|
||||
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
|
||||
default: {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
// Paginierung
|
||||
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">
|
||||
{{ selectOptions.getState(listing.location.state) }}
|
||||
</span>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
<strong>{{ getDaysListed(listing) }} days listed</strong>
|
||||
</p>
|
||||
|
||||
@if (getListingBadge(listing); as badge) {
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -148,7 +148,16 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
|
|||
if (!listing.location) return 'Location not specified';
|
||||
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 {
|
||||
this.router.navigate(['/details-business', listingId]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,29 +4,85 @@
|
|||
|
||||
<!-- Desktop view -->
|
||||
<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">
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<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">Located in</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">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>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<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.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 flex justify-center">
|
||||
{{ listing.internalListingNumber ?? '—' }}
|
||||
</td>
|
||||
<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">
|
||||
{{ listing.draft ? 'Draft' : 'Published' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 px-4">
|
||||
<td class="py-2 px-4 whitespace-nowrap">
|
||||
@if(listing.listingsCategory==='business'){
|
||||
<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">
|
||||
|
|
@ -57,11 +113,27 @@
|
|||
|
||||
<!-- Mobile view -->
|
||||
<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">
|
||||
<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">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">Internal #: {{ listing.internalListingNumber ?? '—' }}</p>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<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">
|
||||
|
|
@ -96,4 +168,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-confirmation></app-confirmation>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,22 @@ import { map2User } from '../../../utils/utils';
|
|||
styleUrl: './my-listing.component.scss',
|
||||
})
|
||||
export class MyListingComponent {
|
||||
listings: Array<ListingType> = []; //dataListings as unknown as Array<BusinessListing>;
|
||||
myListings: Array<ListingType>;
|
||||
// Vollständige, ungefilterte Daten
|
||||
listings: Array<ListingType> = [];
|
||||
// Aktuell angezeigte (gefilterte) Daten
|
||||
myListings: Array<ListingType> = [];
|
||||
|
||||
user: User;
|
||||
|
||||
// VERY small filter state
|
||||
filters = {
|
||||
title: '',
|
||||
internalListingNumber: '',
|
||||
location: '',
|
||||
status: '' as '' | 'published' | 'draft',
|
||||
category: '' as '' | 'business' | 'commercialProperty', // <── NEU
|
||||
};
|
||||
|
||||
constructor(
|
||||
public userService: UserService,
|
||||
private listingsService: ListingsService,
|
||||
|
|
@ -33,23 +46,64 @@ export class MyListingComponent {
|
|||
private confirmationService: ConfirmationService,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const token = await this.authService.getToken();
|
||||
const keycloakUser = map2User(token);
|
||||
const email = keycloakUser.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) {
|
||||
if (listing.listingsCategory === 'business') {
|
||||
await this.listingsService.deleteBusinessListing(listing.id);
|
||||
} 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')]);
|
||||
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.applyFilters(); // Filter beibehalten nach Löschen
|
||||
}
|
||||
|
||||
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