Feature #99 + BugFixes
This commit is contained in:
parent
8dd13d5472
commit
8595e70ceb
|
|
@ -13,7 +13,7 @@ export class AuthController {
|
|||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('users')
|
||||
@Get('user/all')
|
||||
getUsers(): any {
|
||||
return this.authService.getUsers();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}`));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue