Feature #99 + BugFixes

This commit is contained in:
Andreas Knuth 2024-09-14 19:46:18 +02:00
parent 8dd13d5472
commit 8595e70ceb
16 changed files with 626 additions and 24 deletions

View File

@ -13,7 +13,7 @@ export class AuthController {
}
@UseGuards(AdminAuthGuard)
@Get('users')
@Get('user/all')
getUsers(): any {
return this.authService.getUsers();
}

View File

@ -385,6 +385,7 @@ export function createDefaultBusinessListing(): BusinessListing {
};
}
export type StripeSubscription = Stripe.Subscription;
export type StripeUser = Stripe.Customer;
export type IpInfo = {
ip: string;
city: string;
@ -395,3 +396,9 @@ export type IpInfo = {
postal: string;
timezone: string;
};
export interface CombinedUser {
keycloakUser?: KeycloakUser;
appUser?: User;
stripeUser?: StripeUser;
stripeSubscription?: StripeSubscription;
}

View File

@ -1,5 +1,6 @@
import { Body, Controller, Get, HttpException, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
import { Body, Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Param, Post, Req, Res, UseGuards } from '@nestjs/common';
import { Request, Response } from 'express';
import { AdminAuthGuard } from 'src/jwt-auth/admin-auth.guard';
import { Checkout } from 'src/models/main.model';
import Stripe from 'stripe';
import { PaymentService } from './payment.service';
@ -12,6 +13,22 @@ export class PaymentController {
// async createSubscription(@Body() subscriptionData: any) {
// return this.paymentService.createSubscription(subscriptionData);
// }
@UseGuards(AdminAuthGuard)
@Get('user/all')
async getAllStripeCustomer(): Promise<Stripe.Customer[]> {
return this.paymentService.getAllStripeCustomer();
}
@UseGuards(AdminAuthGuard)
@Get('subscription/all')
async getAllStripeSubscriptions(): Promise<Stripe.Subscription[]> {
return this.paymentService.getAllStripeSubscriptions();
}
@UseGuards(AdminAuthGuard)
@Get('paymentmethod/:email')
async getStripePaymentMethods(@Param('email') email: string): Promise<Stripe.PaymentMethod[]> {
return this.paymentService.getStripePaymentMethod(email);
}
@Post('create-checkout-session')
async createCheckoutSession(@Body() checkout: Checkout) {
return this.paymentService.createCheckoutSession(checkout);
@ -40,4 +57,14 @@ export class PaymentController {
async findSubscriptionsById(@Param('email') email: string): Promise<any> {
return await this.paymentService.getSubscription(email);
}
/**
* Endpoint zum Löschen eines Stripe-Kunden.
* Beispiel: DELETE /stripe/customer/cus_12345
*/
@UseGuards(AdminAuthGuard)
@Delete('customer/:id')
@HttpCode(HttpStatus.NO_CONTENT)
async deleteCustomer(@Param('id') customerId: string): Promise<void> {
await this.paymentService.deleteCustomerCompletely(customerId);
}
}

View File

@ -1,4 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { BadRequestException, Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import Stripe from 'stripe';
@ -131,4 +131,89 @@ export class PaymentService {
return [];
}
}
/**
* Ruft alle Stripe-Kunden ab, indem die Paginierung gehandhabt wird.
* @returns Ein Array von Stripe.Customer Objekten.
*/
async getAllStripeCustomer(): Promise<Stripe.Customer[]> {
const allCustomers: Stripe.Customer[] = [];
let hasMore = true;
let startingAfter: string | undefined = undefined;
try {
while (hasMore) {
const response = await this.stripe.customers.list({
limit: 100, // Maximale Anzahl pro Anfrage
starting_after: startingAfter,
});
allCustomers.push(...response.data);
hasMore = response.has_more;
if (hasMore && response.data.length > 0) {
startingAfter = response.data[response.data.length - 1].id;
}
}
return allCustomers;
} catch (error) {
console.error('Fehler beim Abrufen der Stripe-Kunden:', error);
throw new Error('Kunden konnten nicht abgerufen werden.');
}
}
async getAllStripeSubscriptions(): Promise<Stripe.Subscription[]> {
const allSubscriptions: Stripe.Subscription[] = [];
const response = await this.stripe.subscriptions.list({
limit: 100,
});
allSubscriptions.push(...response.data);
return allSubscriptions;
}
async getStripePaymentMethod(email: string): Promise<Stripe.PaymentMethod[]> {
const existingCustomers = await this.stripe.customers.list({
email: email,
limit: 1,
});
const allPayments: Stripe.PaymentMethod[] = [];
if (existingCustomers.data.length > 0) {
const response = await this.stripe.paymentMethods.list({
customer: existingCustomers.data[0].id,
limit: 10,
});
allPayments.push(...response.data);
}
return allPayments;
}
async deleteCustomerCompletely(customerId: string): Promise<void> {
try {
// 1. Abonnements kündigen und löschen
const subscriptions = await this.stripe.subscriptions.list({
customer: customerId,
limit: 100,
});
for (const subscription of subscriptions.data) {
await this.stripe.subscriptions.cancel(subscription.id);
this.logger.info(`Abonnement ${subscription.id} gelöscht.`);
}
// 2. Zahlungsmethoden entfernen
const paymentMethods = await this.stripe.paymentMethods.list({
customer: customerId,
type: 'card',
});
for (const paymentMethod of paymentMethods.data) {
await this.stripe.paymentMethods.detach(paymentMethod.id);
this.logger.info(`Zahlungsmethode ${paymentMethod.id} entfernt.`);
}
// 4. Kunden löschen
await this.stripe.customers.del(customerId);
this.logger.info(`Kunde ${customerId} erfolgreich gelöscht.`);
} catch (error) {
this.logger.error(`Fehler beim Löschen des Kunden ${customerId}:`, error);
throw new InternalServerErrorException('Fehler beim Löschen des Stripe-Kunden.');
}
}
}

View File

@ -1,5 +1,6 @@
import { BadRequestException, Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { AdminAuthGuard } from 'src/jwt-auth/admin-auth.guard';
import { Logger } from 'winston';
import { ZodError } from 'zod';
import { FileService } from '../file/file.service';
@ -18,20 +19,25 @@ export class UserController {
) {}
@UseGuards(OptionalJwtAuthGuard)
@Get()
findByMail(@Request() req, @Query('mail') mail: string): any {
async findByMail(@Request() req, @Query('mail') mail: string): Promise<User> {
this.logger.info(`Searching for user with EMail: ${mail}`);
const user = this.userService.getUserByMail(mail, req.user as JwtUser);
const user = await this.userService.getUserByMail(mail, req.user as JwtUser);
this.logger.info(`Found user: ${JSON.stringify(user)}`);
return user;
}
@Get(':id')
findById(@Param('id') id: string): any {
async findById(@Param('id') id: string): Promise<User> {
this.logger.info(`Searching for user with ID: ${id}`);
const user = this.userService.getUserById(id);
const user = await this.userService.getUserById(id);
this.logger.info(`Found user: ${JSON.stringify(user)}`);
return user;
}
@UseGuards(AdminAuthGuard)
@Get('user/all')
async getAllUser(): Promise<User[]> {
return await this.userService.getAllUser();
}
@Post()
async save(@Body() user: any): Promise<User> {
this.logger.info(`Saving user: ${JSON.stringify(user)}`);
@ -60,9 +66,9 @@ export class UserController {
return savedUser;
}
@Post('search')
find(@Body() criteria: UserListingCriteria): any {
async find(@Body() criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> {
this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);
const foundUsers = this.userService.searchUserListings(criteria);
const foundUsers = await this.userService.searchUserListings(criteria);
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
return foundUsers;
}

View File

@ -54,7 +54,7 @@ export class UserService {
}
return whereConditions;
}
async searchUserListings(criteria: UserListingCriteria) {
async searchUserListings(criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const query = this.conn.select().from(schema.users);
@ -127,6 +127,10 @@ export class UserService {
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user;
}
async getAllUser() {
const users = await this.conn.select().from(schema.users);
return users;
}
async saveUser(user: User, processValidation = true): Promise<User> {
try {
user.updated = new Date();

View File

@ -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' },
];

View File

@ -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

View 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>&nbsp;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>&nbsp;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>

View File

@ -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;
}

View 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;
}
}

View File

@ -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,

View File

@ -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 -->

View File

@ -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" />

View File

@ -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>

View File

@ -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}`));
}
}