Feature #99 + BugFixes
This commit is contained in:
@@ -5,6 +5,7 @@ import { NotFoundComponent } from './components/not-found/not-found.component';
|
||||
import { PaymentComponent } from './components/payment/payment.component';
|
||||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { ListingCategoryGuard } from './guards/listing-category.guard';
|
||||
import { UserListComponent } from './pages/admin/user-list/user-list.component';
|
||||
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
|
||||
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
|
||||
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
|
||||
@@ -158,5 +159,10 @@ export const routes: Routes = [
|
||||
path: 'success',
|
||||
component: SuccessComponent,
|
||||
},
|
||||
{
|
||||
path: 'admin/users',
|
||||
component: UserListComponent,
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{ path: '**', redirectTo: 'home' },
|
||||
];
|
||||
|
||||
@@ -94,6 +94,13 @@
|
||||
<a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
@if(isAdmin()){
|
||||
<ul class="py-2">
|
||||
<li>
|
||||
<a routerLink="admin/users" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Users (Admin)</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
<ul class="py-2 md:hidden">
|
||||
<li>
|
||||
<a
|
||||
|
||||
156
bizmatch/src/app/pages/admin/user-list/user-list.component.html
Normal file
156
bizmatch/src/app/pages/admin/user-list/user-list.component.html
Normal file
@@ -0,0 +1,156 @@
|
||||
<!-- src/app/components/user-list/user-list.component.html -->
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Benutzerverwaltung</h1>
|
||||
|
||||
<!-- Ladeanzeige -->
|
||||
<div *ngIf="isLoading" class="flex justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
|
||||
<!-- Fehlermeldung -->
|
||||
<div *ngIf="error" class="text-red-500 mb-4">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Benutzer-Tabelle -->
|
||||
<table *ngIf="!isLoading && !error" class="min-w-full bg-white border">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- <th class="py-2 px-4 border-b">ID</th> -->
|
||||
<th class="py-2 px-4 border-b">Vorname</th>
|
||||
<th class="py-2 px-4 border-b">Nachname</th>
|
||||
<th class="py-2 px-4 border-b">E-Mail</th>
|
||||
<th class="py-2 px-4 border-b">DB</th>
|
||||
<th class="py-2 px-4 border-b">Keycloak</th>
|
||||
<th class="py-2 px-4 border-b">Stripe</th>
|
||||
<th class="py-2 px-4 border-b">Sub</th>
|
||||
<th class="py-2 px-4 border-b">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let user of combinedUsers; let i = index" class="text-center">
|
||||
<td class="py-2 px-4 border-b">
|
||||
{{ user.appUser?.firstname || user.keycloakUser?.firstName || user.stripeUser?.name || '—' }}
|
||||
</td>
|
||||
<td class="py-2 px-4 border-b">
|
||||
{{ user.appUser?.lastname || user.keycloakUser?.lastName || '—' }}
|
||||
</td>
|
||||
<td class="py-2 px-4 border-b">
|
||||
{{ user.appUser?.email || user.keycloakUser?.email || user.stripeUser?.email }}
|
||||
</td>
|
||||
<td class="py-2 px-4 border-b">
|
||||
<input type="checkbox" [checked]="!!user.appUser" disabled />
|
||||
</td>
|
||||
<td class="py-2 px-4 border-b">
|
||||
<input type="checkbox" [checked]="!!user.keycloakUser" disabled />
|
||||
</td>
|
||||
<td class="py-2 px-4 border-b">
|
||||
<input type="checkbox" [checked]="!!user.stripeUser" disabled />
|
||||
</td>
|
||||
<td class="py-2 px-4 border-b">
|
||||
@if(!!user.stripeSubscription){
|
||||
<input type="checkbox" [checked]="!!user.stripeSubscription" disabled attr.data-tooltip-target="tooltip-{{ i }}" />
|
||||
}@else {
|
||||
<input type="checkbox" [checked]="!!user.stripeSubscription" disabled />
|
||||
} @if(!!user.stripeSubscription){
|
||||
<app-tooltip id="tooltip-{{ i }}" [text]="getSubscriptionInfo(user.stripeSubscription)"></app-tooltip>
|
||||
}
|
||||
</td>
|
||||
<td class="py-2 px-4 border-b space-x-2">
|
||||
<button class="share share-delete text-white font-bold text-xs py-1 px-2 inline-flex items-center" attr.data-dropdown-toggle="dropdown_{{ user.appUser?.id }}">
|
||||
Delete<svg class="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Dropdown menu -->
|
||||
<div id="dropdown_{{ user.appUser?.id }}" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44">
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDefaultButton">
|
||||
<li>
|
||||
<a class="block px-4 py-2 hover:bg-gray-100" (click)="delete(user)">Complete</a>
|
||||
</li>
|
||||
@if(user.stripeSubscription){
|
||||
<li>
|
||||
<a class="block px-4 py-2 hover:bg-gray-100" (click)="deleteFromStripe(user)">From Stripe</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<button class="share share-cc text-white font-bold text-xs py-1 px-2 inline-flex items-center" (click)="showCreditCardInfo(user)" [disabled]="!user.stripeSubscription">
|
||||
<i class="fa-solid fa-credit-card"></i> CC Info
|
||||
</button>
|
||||
<button class="share share-msg text-white font-bold text-xs py-1 px-2 inline-flex items-center" (click)="showMessages(user)"><i class="fa-solid fa-message"></i> Messages</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Flowbite Modal für Kreditkarteninformationen -->
|
||||
<div *ngIf="showModal" class="fixed top-0 left-0 right-0 z-50 flex items-center justify-center w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full" aria-modal="true" role="dialog">
|
||||
<div class="relative w-full max-w-2xl max-h-full">
|
||||
<!-- Modal-Content -->
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<!-- Modal-Kopf -->
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Kreditkarteninformationen</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
(click)="closeModal()"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Modal-Körper -->
|
||||
<div class="p-6 space-y-6">
|
||||
<div *ngIf="ccInfoLoading" class="flex justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="ccInfoError" class="text-red-500">
|
||||
{{ ccInfoError }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="!ccInfoLoading && !ccInfoError">
|
||||
<ng-container *ngIf="creditCardInfo.length > 0; else noCCInfo">
|
||||
<table class="min-w-full bg-white border">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2 px-4 border-b">Kartenmarke</th>
|
||||
<th class="py-2 px-4 border-b">Letzte 4</th>
|
||||
<th class="py-2 px-4 border-b">Ablaufdatum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let method of creditCardInfo" class="text-center">
|
||||
<td class="py-2 px-4 border-b">{{ method.card?.brand || '—' }}</td>
|
||||
<td class="py-2 px-4 border-b">{{ method.card?.last4 || '—' }}</td>
|
||||
<td class="py-2 px-4 border-b">{{ method.card?.exp_month }}/{{ method.card?.exp_year }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-template #noCCInfo>
|
||||
<p>Keine Kreditkarteninformationen verfügbar.</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal-Fuß -->
|
||||
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||
<button
|
||||
type="button"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
(click)="closeModal()"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,64 @@
|
||||
// button.share {
|
||||
// font-size: 13px;
|
||||
// transform: translateY(-2px) scale(1.03);
|
||||
// margin-right: 4px;
|
||||
// margin-left: 2px;
|
||||
// border-radius: 4px;
|
||||
// i {
|
||||
// font-size: 15px;
|
||||
// }
|
||||
// }
|
||||
// .share-msg {
|
||||
// background-color: #0088cc;
|
||||
// }
|
||||
// .share-delete {
|
||||
// background-color: #e60023;
|
||||
// }
|
||||
// .share-cc {
|
||||
// background-color: #ff961c;
|
||||
// }
|
||||
button.share {
|
||||
font-size: 13px;
|
||||
transform: translateY(-2px) scale(1.03);
|
||||
margin-right: 4px;
|
||||
margin-left: 2px;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s, background-color 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
button.share i {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
button.share-delete {
|
||||
background-color: #e60023; /* Rot */
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.share-delete:hover:not(:disabled) {
|
||||
background-color: #cc001f; /* Dunkleres Rot für Hover */
|
||||
}
|
||||
|
||||
button.share-cc {
|
||||
background-color: #4f46e5; /* Beispiel: Indigo für CC Info */
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.share-cc:hover:not(:disabled) {
|
||||
background-color: #4338ca; /* Dunkleres Indigo für Hover */
|
||||
}
|
||||
|
||||
button.share-msg {
|
||||
background-color: #10b981; /* Beispiel: Grün für Messages */
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.share-msg:hover:not(:disabled) {
|
||||
background-color: #059669; /* Dunkleres Grün für Hover */
|
||||
}
|
||||
|
||||
/* Deaktivierter Zustand */
|
||||
button.share:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
138
bizmatch/src/app/pages/admin/user-list/user-list.component.ts
Normal file
138
bizmatch/src/app/pages/admin/user-list/user-list.component.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import { PaymentMethod } from '@stripe/stripe-js';
|
||||
import { initFlowbite } from 'flowbite';
|
||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { CombinedUser, StripeSubscription } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
||||
import { MessageService } from '../../../components/message/message.service';
|
||||
import { TooltipComponent } from '../../../components/tooltip/tooltip.component';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TooltipComponent],
|
||||
providers: [DatePipe],
|
||||
templateUrl: './user-list.component.html',
|
||||
styleUrl: './user-list.component.scss',
|
||||
})
|
||||
export class UserListComponent implements OnInit {
|
||||
combinedUsers: CombinedUser[] = [];
|
||||
isLoading = true;
|
||||
error: string | null = null;
|
||||
selectedUser: CombinedUser | null = null;
|
||||
creditCardInfo: PaymentMethod[] = [];
|
||||
ccInfoLoading = false;
|
||||
ccInfoError: string | null = null;
|
||||
showModal = false;
|
||||
constructor(private userService: UserService, private datePipe: DatePipe, private confirmationService: ConfirmationService, private messageService: MessageService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadUsers();
|
||||
}
|
||||
ngAfterViewInit() {
|
||||
// initFlowbite();
|
||||
}
|
||||
loadUsers(): void {
|
||||
this.userService.loadUsers().subscribe({
|
||||
next: users => {
|
||||
this.combinedUsers = users;
|
||||
this.isLoading = false;
|
||||
setTimeout(() => {
|
||||
initFlowbite();
|
||||
}, 10);
|
||||
},
|
||||
error: err => {
|
||||
this.error = 'Fehler beim Laden der Benutzer';
|
||||
this.isLoading = false;
|
||||
console.error(err);
|
||||
},
|
||||
});
|
||||
}
|
||||
getSubscriptionInfo(subscription: StripeSubscription) {
|
||||
return `${subscription.metadata['plan']} / ${subscription.status} / ${this.datePipe.transform(new Date(subscription.start_date * 1000))} / ${this.datePipe.transform(
|
||||
new Date(subscription.current_period_end * 1000),
|
||||
)}`;
|
||||
}
|
||||
async deleteFromStripe(user: CombinedUser) {
|
||||
const confirmed = await this.confirmationService.showConfirmation({ message: `Do you want to delete the User from Stripe ?` });
|
||||
if (confirmed) {
|
||||
if (!user || !user.stripeUser) {
|
||||
// Benutzer oder StripeUser nicht definiert
|
||||
return;
|
||||
}
|
||||
|
||||
const customerId = user.stripeUser.id; // Angenommen, 'id' ist die Kunden-ID
|
||||
|
||||
try {
|
||||
// 1. Stripe User löschen
|
||||
await this.userService.deleteCustomerFromStripe(customerId);
|
||||
console.log('Stripe User erfolgreich gelöscht.');
|
||||
|
||||
// 2. App-User aktualisieren
|
||||
const appUser = user.appUser;
|
||||
if (appUser) {
|
||||
const updatedUser: User = {
|
||||
...appUser,
|
||||
subscriptionId: null,
|
||||
customerType: 'buyer',
|
||||
subscriptionPlan: 'free',
|
||||
customerSubType: null,
|
||||
};
|
||||
|
||||
const savedUser = await this.userService.saveGuaranteed(updatedUser);
|
||||
console.log('App-User erfolgreich aktualisiert:', savedUser);
|
||||
}
|
||||
this.messageService.addMessage({
|
||||
severity: 'success',
|
||||
text: 'Stripe User deleted.',
|
||||
duration: 3000, // 3 seconds
|
||||
});
|
||||
// Optional: Aktualisieren Sie die Benutzerliste oder führen Sie andere Aktionen aus
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Benutzers:', error);
|
||||
this.messageService.addMessage({
|
||||
severity: 'danger',
|
||||
text: 'Error is occured during the deletion of the user ...',
|
||||
duration: 3000, // 3 seconds
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(user: CombinedUser): void {}
|
||||
showCreditCardInfo(user: CombinedUser): void {
|
||||
this.selectedUser = user;
|
||||
this.creditCardInfo = [];
|
||||
this.ccInfoError = null;
|
||||
this.ccInfoLoading = true;
|
||||
this.showModal = true;
|
||||
|
||||
const email = user.appUser?.email || user.keycloakUser?.email || user.stripeUser?.email;
|
||||
if (email) {
|
||||
this.userService.getPaymentMethods(email).subscribe({
|
||||
next: methods => {
|
||||
this.creditCardInfo = methods;
|
||||
this.ccInfoLoading = false;
|
||||
},
|
||||
error: err => {
|
||||
this.ccInfoError = 'Fehler beim Laden der Kreditkarteninformationen';
|
||||
this.ccInfoLoading = false;
|
||||
console.error(err);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.ccInfoError = 'Keine gültige E-Mail-Adresse gefunden';
|
||||
this.ccInfoLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
showMessages(user: CombinedUser): void {}
|
||||
closeModal(): void {
|
||||
this.showModal = false;
|
||||
this.selectedUser = null;
|
||||
this.creditCardInfo = [];
|
||||
this.ccInfoError = null;
|
||||
}
|
||||
}
|
||||
@@ -175,8 +175,8 @@ export class DetailsCommercialPropertyListingComponent {
|
||||
}
|
||||
async showShareByEMail() {
|
||||
const result = await this.emailService.showShareByEMail({
|
||||
yourEmail: this.user.email,
|
||||
yourName: `${this.user.firstname} ${this.user.lastname}`,
|
||||
yourEmail: this.user ? this.user.email : null,
|
||||
yourName: this.user ? `${this.user.firstname} ${this.user.lastname}` : null,
|
||||
url: environment.mailinfoUrl,
|
||||
listingTitle: this.listing.title,
|
||||
id: this.listing.id,
|
||||
|
||||
@@ -54,13 +54,15 @@
|
||||
<span class="p-2 flex-grow">{{ user.firstname }} {{ user.lastname }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center">
|
||||
<span class="font-semibold w-40 p-2">Phone Number</span>
|
||||
<span class="p-2 flex-grow">{{ formatPhoneNumber(user.phoneNumber) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center bg-gray-100">
|
||||
<span class="font-semibold w-40 p-2">EMail Address</span>
|
||||
<span class="p-2 flex-grow">{{ user.email }}</span>
|
||||
</div>
|
||||
@if(user.customerType==='professional'){
|
||||
<div class="flex flex-col sm:flex-row sm:items-center bg-gray-100">
|
||||
<span class="font-semibold w-40 p-2">Phone Number</span>
|
||||
<span class="p-2 flex-grow">{{ formatPhoneNumber(user.phoneNumber) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center">
|
||||
<span class="font-semibold w-40 p-2">Company Location</span>
|
||||
<span class="p-2 flex-grow">{{ user.location?.name }} - {{ user.location?.state }}</span>
|
||||
@@ -69,8 +71,9 @@
|
||||
<span class="font-semibold w-40 p-2">Professional Type</span>
|
||||
<span class="p-2 flex-grow">{{ selectOptions.getCustomerSubType(user.customerSubType) }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if(user.customerType==='professional'){
|
||||
<!-- Services -->
|
||||
<div class="mt-6">
|
||||
<h3 class="font-semibold mb-2">Services we offer</h3>
|
||||
@@ -94,6 +97,7 @@
|
||||
<span class="bg-green-100 text-green-800 px-2 py-1 rounded-full text-sm">{{ license.registerNo }}-{{ license.state }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Business Listings -->
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
<div class="mt-4 flex items-center justify-center text-gray-700">
|
||||
<span class="mr-2">AI-Search</span>
|
||||
<span [attr.data-tooltip-target]="tooltipTargetBeta" class="bg-sky-300 text-teal-800 text-xs font-semibold px-2 py-1 rounded">BETA</span>
|
||||
<app-tooltip [id]="tooltipTargetBeta" text="The AI will convert your input into filter criteria. Please check them in the filter menu after the search"></app-tooltip>
|
||||
<app-tooltip [id]="tooltipTargetBeta" text="AI will convert your input into filter criteria. Please check them in the filter menu after search"></app-tooltip>
|
||||
<span class="ml-2">- Try now</span>
|
||||
<div class="ml-4 relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
||||
<input (click)="toggleAiSearch()" type="checkbox" name="toggle" id="toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 border-gray-300 appearance-none cursor-pointer" />
|
||||
|
||||
@@ -295,7 +295,7 @@
|
||||
<div class="flex justify-start">
|
||||
<button
|
||||
routerLink="/pricing"
|
||||
class="py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
class="py-2.5 px-5 me-2 mb-2 text-sm font-medium text-white focus:outline-none bg-green-500 rounded-lg border border-gray-400 hover:bg-green-600 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
Upgrade Subscription Plan
|
||||
</button>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, lastValueFrom, Observable } from 'rxjs';
|
||||
import { PaymentMethod } from '@stripe/stripe-js';
|
||||
import { BehaviorSubject, catchError, forkJoin, lastValueFrom, map, Observable, of } from 'rxjs';
|
||||
import urlcat from 'urlcat';
|
||||
import { User } from '../../../../bizmatch-server/src/models/db.model';
|
||||
import { ResponseUsersArray, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
|
||||
import { CombinedUser, KeycloakUser, ResponseUsersArray, StripeSubscription, StripeUser, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
@@ -46,7 +47,104 @@ export class UserService {
|
||||
getNumberOfBroker(criteria?: UserListingCriteria): Observable<number> {
|
||||
return this.http.post<number>(`${this.apiBaseUrl}/bizmatch/user/findTotal`, criteria);
|
||||
}
|
||||
// async getAllStates(): Promise<any> {
|
||||
// return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`));
|
||||
// }
|
||||
// -------------------------------
|
||||
// ADMIN SERVICES
|
||||
// -------------------------------
|
||||
getKeycloakUsers(): Observable<KeycloakUser[]> {
|
||||
return this.http.get<KeycloakUser[]>(`${this.apiBaseUrl}/bizmatch/auth/user/all`).pipe(
|
||||
catchError(error => {
|
||||
console.error('Fehler beim Laden der Keycloak-Benutzer', error);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getAppUsers(): Observable<User[]> {
|
||||
return this.http.get<User[]>(`${this.apiBaseUrl}/bizmatch/user/user/all`).pipe(
|
||||
catchError(error => {
|
||||
console.error('Fehler beim Laden der App-Benutzer', error);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getAllStripeSubscriptions(): Observable<StripeSubscription[]> {
|
||||
return this.http.get<StripeSubscription[]>(`${this.apiBaseUrl}/bizmatch/payment/subscription/all`).pipe(
|
||||
catchError(error => {
|
||||
console.error('Fehler beim Laden der Stripe-Subscriptions', error);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getAllStripeUsers(): Observable<StripeUser[]> {
|
||||
return this.http.get<StripeUser[]>(`${this.apiBaseUrl}/bizmatch/payment/user/all`).pipe(
|
||||
catchError(error => {
|
||||
console.error('Fehler beim Laden der Stripe-Benutzer', error);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
getPaymentMethods(email: string): Observable<PaymentMethod[]> {
|
||||
return this.http.get<PaymentMethod[]>(`${this.apiBaseUrl}/bizmatch/payment/paymentmethod/${email}`).pipe(
|
||||
catchError(error => {
|
||||
console.error('Fehler beim Laden der Zahlungsinformationen', error);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Lädt alle Benutzer aus den verschiedenen Quellen und kombiniert sie.
|
||||
* @returns Ein Observable mit einer Liste von CombinedUser.
|
||||
*/
|
||||
loadUsers(): Observable<CombinedUser[]> {
|
||||
return forkJoin({
|
||||
keycloakUsers: this.getKeycloakUsers(),
|
||||
appUsers: this.getAppUsers(),
|
||||
stripeSubscriptions: this.getAllStripeSubscriptions(),
|
||||
stripeUsers: this.getAllStripeUsers(),
|
||||
}).pipe(
|
||||
map(({ keycloakUsers, appUsers, stripeSubscriptions, stripeUsers }) => {
|
||||
const combinedUsers: CombinedUser[] = [];
|
||||
|
||||
// Map App Users mit Keycloak und Stripe Subscription
|
||||
appUsers.forEach(appUser => {
|
||||
const keycloakUser = keycloakUsers.find(kcUser => kcUser.email.toLowerCase() === appUser.email.toLowerCase());
|
||||
|
||||
// const stripeSubscription = appUser.subscriptionId ? stripeSubscriptions.find(sub => sub.id === appUser.subscriptionId) : null;
|
||||
const stripeUser = stripeUsers.find(suser => suser.email === appUser.email);
|
||||
const stripeSubscription = stripeUser ? stripeSubscriptions.find(sub => sub.customer === stripeUser.id) : null;
|
||||
combinedUsers.push({
|
||||
appUser,
|
||||
keycloakUser,
|
||||
stripeUser,
|
||||
stripeSubscription,
|
||||
});
|
||||
});
|
||||
|
||||
// Füge Stripe-Benutzer hinzu, die nicht in App oder Keycloak vorhanden sind
|
||||
stripeUsers.forEach(stripeUser => {
|
||||
const existsInApp = appUsers.some(appUser => appUser.email.toLowerCase() === stripeUser.email.toLowerCase());
|
||||
const existsInKeycloak = keycloakUsers.some(kcUser => kcUser.email.toLowerCase() === stripeUser.email.toLowerCase());
|
||||
|
||||
if (!existsInApp && !existsInKeycloak) {
|
||||
combinedUsers.push({
|
||||
stripeUser,
|
||||
// Optional: Verknüpfe Stripe-Benutzer mit ihren Subscriptions
|
||||
stripeSubscription: stripeSubscriptions.find(sub => sub.customer === stripeUser.id) || null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return combinedUsers;
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error('Fehler beim Kombinieren der Benutzer', err);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
async deleteCustomerFromStripe(customerId: string): Promise<void> {
|
||||
await lastValueFrom(this.http.delete(`${this.apiBaseUrl}/bizmatch/payment/customer/${customerId}`));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user