Validierung Part II, neue Komponenten

This commit is contained in:
2024-08-01 22:43:32 +02:00
parent 2955c034a0
commit 29f88d610f
18 changed files with 1431 additions and 242 deletions

View File

@@ -0,0 +1,50 @@
import { Component, Input } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { Subscription } from 'rxjs';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-base-input',
template: ``,
standalone: true,
imports: [],
})
export abstract class BaseInputComponent implements ControlValueAccessor {
@Input() value: any = '';
validationMessage: string = '';
onChange: any = () => {};
onTouched: any = () => {};
subscription: Subscription | null = null;
@Input() label: string = '';
// @Input() id: string = '';
@Input() name: string = '';
constructor(protected validationMessagesService: ValidationMessagesService) {}
ngOnInit() {
this.subscription = this.validationMessagesService.messages$.subscribe(() => {
this.updateValidationMessage();
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
writeValue(value: any): void {
if (value !== undefined) {
this.value = value;
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
updateValidationMessage(): void {
this.validationMessage = this.validationMessagesService.getMessage(this.name);
}
setDisabledState?(isDisabled: boolean): void {}
}

View File

@@ -1,22 +1,24 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Component, EventEmitter, forwardRef, Output } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { BaseInputComponent } from '../base-input/base-input.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-input',
template: `
<div>
<label [for]="id" class="block text-sm font-medium text-gray-700">
<label [for]="name" class="block text-sm font-medium text-gray-700">
{{ label }}
<span class="text-red-500 ml-1">{{ validationMessage }}</span>
</label>
<input
type="text"
[id]="name"
[ngModel]="value"
(input)="onInputChange($event)"
(blur)="onTouched()"
[attr.name]="name"
[required]="required"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
@@ -31,32 +33,11 @@ import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/f
},
],
})
export class ValidatedInputComponent implements ControlValueAccessor {
@Input() label: string = '';
@Input() id: string = '';
@Input() name: string = '';
@Input() type: string = 'text';
@Input() required: boolean = false;
@Input() validationMessage: string = '';
@Input() value: any = '';
export class ValidatedInputComponent extends BaseInputComponent {
@Output() valueChange = new EventEmitter<any>();
onChange: any = () => {};
onTouched: any = () => {};
writeValue(value: any): void {
if (value !== undefined) {
this.value = value;
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onInputChange(event: Event): void {
@@ -65,8 +46,4 @@ export class ValidatedInputComponent implements ControlValueAccessor {
this.onChange(value);
this.valueChange.emit(value);
}
setDisabledState?(isDisabled: boolean): void {
// Implementieren Sie dies, wenn Sie die Deaktivierung des Inputs unterstützen möchten
}
}

View File

@@ -0,0 +1,41 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { QuillModule } from 'ngx-quill';
import { BaseInputComponent } from '../base-input/base-input.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-quill',
template: `
<div>
<label [for]="name" class="block text-sm font-medium text-gray-700">
{{ label }}
<span class="text-red-500 ml-1">{{ validationMessage }}</span>
</label>
<quill-editor [(ngModel)]="value" (ngModelChange)="onInputChange($event)" (onBlur)="onTouched()" [id]="name" [attr.name]="name" [modules]="quillModules"></quill-editor>
</div>
`,
standalone: true,
imports: [CommonModule, FormsModule, QuillModule],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedQuillComponent),
multi: true,
},
],
})
export class ValidatedQuillComponent extends BaseInputComponent {
quillModules = {
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
};
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onInputChange(event: Event): void {
this.value = event;
this.onChange(this.value);
}
}

View File

@@ -1,22 +1,23 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { BaseInputComponent } from '../base-input/base-input.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-select',
template: `
<div>
<label [for]="id" class="block text-sm font-medium text-gray-700">
<label [for]="name" class="block text-sm font-medium text-gray-700">
{{ label }}
<span class="text-red-500 ml-1">{{ validationMessage }}</span>
</label>
<select
[id]="id"
[id]="name"
[name]="name"
[ngModel]="value"
(change)="onSelectChange($event)"
(blur)="onTouched()"
[required]="required"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
>
<option value="" disabled selected>Select an option</option>
@@ -36,32 +37,14 @@ import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/f
},
],
})
export class ValidatedSelectComponent implements ControlValueAccessor {
@Input() label: string = '';
@Input() id: string = '';
@Input() name: string = '';
@Input() required: boolean = false;
@Input() validationMessage: string = '';
export class ValidatedSelectComponent extends BaseInputComponent {
// @Input() required: boolean = false;
// @Input() validationMessage: string = '';
@Input() options: Array<{ value: any; label: string }> = [];
@Input() value: any = '';
@Output() valueChange = new EventEmitter<any>();
onChange: any = () => {};
onTouched: any = () => {};
writeValue(value: any): void {
if (value !== undefined) {
this.value = value;
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onSelectChange(event: Event): void {
@@ -70,8 +53,4 @@ export class ValidatedSelectComponent implements ControlValueAccessor {
this.onChange(value);
this.valueChange.emit(value);
}
setDisabledState?(isDisabled: boolean): void {
// Implementieren Sie dies, wenn Sie die Deaktivierung des Selects unterstützen möchten
}
}

View File

@@ -0,0 +1,39 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, forwardRef, Output } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { BaseInputComponent } from '../base-input/base-input.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-textarea',
template: `
<div>
<label [for]="name" class="block text-sm font-medium text-gray-700">
{{ label }}
<span class="text-red-500 ml-1">{{ validationMessage }}</span>
</label>
<textarea [id]="name" [ngModel]="value" (ngModelChange)="onInputChange($event)" [attr.name]="name" class="w-full p-2 border border-gray-300 rounded-md" rows="3"></textarea>
</div>
`,
standalone: true,
imports: [CommonModule, FormsModule],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedTextareaComponent),
multi: true,
},
],
})
export class ValidatedTextareaComponent extends BaseInputComponent {
@Output() valueChange = new EventEmitter<any>();
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}
onInputChange(event: Event): void {
this.value = event;
this.onChange(this.value);
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable, InjectionToken } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface ValidationMessage {
field: string;
message: string;
}
export const VALIDATION_MESSAGES = new InjectionToken<ValidationMessagesService>('VALIDATION_MESSAGES');
@Injectable({
providedIn: 'root',
})
export class ValidationMessagesService {
private messagesSubject = new BehaviorSubject<ValidationMessage[]>([]);
messages$: Observable<ValidationMessage[]> = this.messagesSubject.asObservable();
updateMessages(messages: ValidationMessage[]): void {
this.messagesSubject.next(messages);
}
clearMessages(): void {
this.messagesSubject.next([]);
}
getMessage(field: string): string | null {
const messages = this.messagesSubject.value;
const message = messages.find(m => m.field === field);
return message ? message.message : null;
}
}

View File

@@ -60,8 +60,8 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<app-validated-input label="First Name" name="firstname" [(ngModel)]="user.firstname" [required]="true" [validationMessage]="getValidationMessage('firstname')"></app-validated-input>
<app-validated-input label="Last Name" name="lastname" [(ngModel)]="user.lastname" [required]="true" [validationMessage]="getValidationMessage('lastname')"></app-validated-input>
<app-validated-input label="First Name" name="firstname" [(ngModel)]="user.firstname"></app-validated-input>
<app-validated-input label="Last Name" name="lastname" [(ngModel)]="user.lastname"></app-validated-input>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -71,22 +71,15 @@
<option *ngFor="let type of customerTypes" [value]="type">{{ type | titlecase }}</option>
</select>
</div> -->
<app-validated-select
label="Customer Type"
id="customerType"
name="customerType"
[(ngModel)]="user.customerType"
[required]="true"
[validationMessage]="getValidationMessage('customerType')"
[options]="customerTypeOptions"
></app-validated-select>
<app-validated-select label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
@if (isProfessional){
<div>
<!-- <div>
<label for="customerSubType" class="block text-sm font-medium text-gray-700">Professional Type</label>
<select id="customerSubType" name="customerSubType" [(ngModel)]="user.customerSubType" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option *ngFor="let subType of customerSubTypes" [value]="subType">{{ subType | titlecase }}</option>
</select>
</div>
</div> -->
<app-validated-select label="Professional Type" name="customerSubType" [(ngModel)]="user.customerSubType" [options]="customerSubTypeOptions"></app-validated-select>
}
</div>
@if (isProfessional){
@@ -116,11 +109,11 @@
</div>
</div>
<div>
<!-- <div>
<label for="companyOverview" class="block text-sm font-medium text-gray-700">Company Overview</label>
<quill-editor [(ngModel)]="user.companyOverview" name="companyOverview" [modules]="quillModules"></quill-editor>
</div>
</div> -->
<app-validated-quill label="Company Overview" name="companyOverview" [(ngModel)]="user.companyOverview" (ngModelChange)="test($event)"></app-validated-quill>
<div>
<label for="offeredServices" class="block text-sm font-medium text-gray-700">Services We Offer</label>
<quill-editor [(ngModel)]="user.offeredServices" name="offeredServices" [modules]="quillModules"></quill-editor>

View File

@@ -9,7 +9,7 @@ import { ImageCropperComponent } from 'ngx-image-cropper';
import { QuillModule } from 'ngx-quill';
import { lastValueFrom } from 'rxjs';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, Invoice, Subscription, UploadParams, ValidationMessage, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { AutoCompleteCompleteEvent, Invoice, Subscription, UploadParams, ValidationMessage, createDefaultUser, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
@@ -17,7 +17,9 @@ import { ImageCropAndUploadComponent, UploadReponse } from '../../../components/
import { MessageComponent } from '../../../components/message/message.component';
import { MessageService } from '../../../components/message/message.service';
import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
import { ValidatedQuillComponent } from '../../../components/validated-quill/validated-quill.component';
import { ValidatedSelectComponent } from '../../../components/validated-select/validated-select.component';
import { ValidationMessagesService } from '../../../components/validation-messages.service';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
import { LoadingService } from '../../../services/loading.service';
@@ -26,7 +28,7 @@ import { SharedService } from '../../../services/shared.service';
import { SubscriptionsService } from '../../../services/subscriptions.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { createDefaultUser, map2User } from '../../../utils/utils';
import { map2User } from '../../../utils/utils';
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({
selector: 'app-account',
@@ -42,6 +44,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
MessageComponent,
ValidatedInputComponent,
ValidatedSelectComponent,
ValidatedQuillComponent,
],
providers: [TitleCasePipe],
templateUrl: './account.component.html',
@@ -83,6 +86,7 @@ export class AccountComponent {
private messageService: MessageService,
private sharedService: SharedService,
private titleCasePipe: TitleCasePipe,
private validationMessagesService: ValidationMessagesService,
) {}
async ngOnInit() {
if (this.id) {
@@ -91,23 +95,7 @@ export class AccountComponent {
const token = await this.keycloakService.getToken();
const keycloakUser = map2User(token);
const email = keycloakUser.email;
try {
this.user = await this.userService.getByMail(email);
} catch (e) {
this.user = {
id: undefined,
email,
firstname: keycloakUser.firstName,
lastname: keycloakUser.lastName,
areasServed: [],
licensedIn: [],
companyOverview: '',
offeredServices: '',
customerType: 'professional',
customerSubType: 'broker',
};
this.user = await this.userService.save(this.user);
}
this.user = await this.userService.getByMail(email);
}
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.id));
@@ -135,8 +123,20 @@ export class AccountComponent {
this.imageService.deleteLogoImagesByMail(this.user.email);
this.imageService.deleteProfileImagesByMail(this.user.email);
}
await this.userService.save(this.user);
this.messageService.addMessage({ severity: 'success', text: 'Account changes have been persisted', duration: 3000 });
try {
await this.userService.save(this.user);
this.messageService.addMessage({ severity: 'success', text: 'Account changes have been persisted', duration: 3000 });
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
} catch (error) {
this.messageService.addMessage({
severity: 'danger',
text: 'An error occurred while saving the profile',
duration: 5000,
});
if (error.error && Array.isArray(error.error?.message)) {
this.validationMessagesService.updateMessages(error.error.message);
}
}
}
onUploadCompanyLogo(event: any) {
@@ -214,4 +214,7 @@ export class AccountComponent {
const message = this.validationMessages.find(msg => msg.field === fieldName);
return message ? message.message : '';
}
test(value) {
console.log(`--->${value}`);
}
}

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { createDefaultBusinessListing, map2User, routeListingWithState } from '../../../utils/utils';
import { map2User, routeListingWithState } from '../../../utils/utils';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
@@ -13,7 +13,8 @@ import { QuillModule } from 'ngx-quill';
import { NgSelectModule } from '@ng-select/ng-select';
import { NgxCurrencyDirective } from 'ngx-currency';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { AutoCompleteCompleteEvent, ImageProperty, createDefaultBusinessListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { MessageService } from '../../../components/message/message.service';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { createDefaultCommercialPropertyListing, map2User, routeListingWithState } from '../../../utils/utils';
import { map2User, routeListingWithState } from '../../../utils/utils';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { ViewportRuler } from '@angular/cdk/scrolling';
@@ -14,7 +14,8 @@ import { NgxCurrencyDirective } from 'ngx-currency';
import { ImageCroppedEvent, ImageCropperComponent } from 'ngx-image-cropper';
import { QuillModule } from 'ngx-quill';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { AutoCompleteCompleteEvent, ImageProperty, createDefaultCommercialPropertyListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';

View File

@@ -1,94 +1,93 @@
import { Router } from '@angular/router';
import { ConsoleFormattedStream, INFO, createLogger as _createLogger, stdSerializers } from 'browser-bunyan';
import { jwtDecode } from 'jwt-decode';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, JwtToken, KeycloakUser, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
export function createDefaultUser(email: string, firstname: string, lastname: string): User {
return {
id: undefined,
email,
firstname,
lastname,
phoneNumber: '',
description: '',
companyName: '',
companyOverview: '',
companyWebsite: '',
companyLocation: '',
offeredServices: '',
areasServed: [],
hasProfile: false,
hasCompanyLogo: false,
licensedIn: [],
gender: undefined,
customerType: undefined,
customerSubType: undefined,
created: new Date(),
updated: new Date(),
};
}
// export function createDefaultUser(email: string, firstname: string, lastname: string): User {
// return {
// id: undefined,
// email,
// firstname,
// lastname,
// phoneNumber: '',
// description: '',
// companyName: '',
// companyOverview: '',
// companyWebsite: '',
// companyLocation: '',
// offeredServices: '',
// areasServed: [],
// hasProfile: false,
// hasCompanyLogo: false,
// licensedIn: [],
// gender: undefined,
// customerType: undefined,
// customerSubType: undefined,
// created: new Date(),
// updated: new Date(),
// };
// }
export function createDefaultCommercialPropertyListing(): CommercialPropertyListing {
return {
id: undefined,
serialId: undefined,
email: '',
type: null,
title: '',
description: '',
city: '',
state: '',
price: null,
favoritesForUser: [],
hideImage: false,
draft: false,
zipCode: null,
county: '',
imageOrder: [],
imagePath: '',
created: null,
updated: null,
visits: null,
lastVisit: null,
latitude: null,
longitude: null,
listingsCategory: 'commercialProperty',
};
}
export function createDefaultBusinessListing(): BusinessListing {
return {
id: undefined,
email: '',
type: null,
title: '',
description: '',
city: '',
state: '',
price: null,
favoritesForUser: [],
draft: false,
realEstateIncluded: false,
leasedLocation: false,
franchiseResale: false,
salesRevenue: null,
cashFlow: null,
supportAndTraining: '',
employees: null,
established: null,
internalListingNumber: null,
reasonForSale: '',
brokerLicencing: '',
internals: '',
created: null,
updated: null,
visits: null,
lastVisit: null,
latitude: null,
longitude: null,
listingsCategory: 'business',
};
}
// export function createDefaultCommercialPropertyListing(): CommercialPropertyListing {
// return {
// id: undefined,
// serialId: undefined,
// email: '',
// type: null,
// title: '',
// description: '',
// city: '',
// state: '',
// price: null,
// favoritesForUser: [],
// hideImage: false,
// draft: false,
// zipCode: null,
// county: '',
// imageOrder: [],
// imagePath: '',
// created: null,
// updated: null,
// visits: null,
// lastVisit: null,
// latitude: null,
// longitude: null,
// listingsCategory: 'commercialProperty',
// };
// }
// export function createDefaultBusinessListing(): BusinessListing {
// return {
// id: undefined,
// email: '',
// type: null,
// title: '',
// description: '',
// city: '',
// state: '',
// price: null,
// favoritesForUser: [],
// draft: false,
// realEstateIncluded: false,
// leasedLocation: false,
// franchiseResale: false,
// salesRevenue: null,
// cashFlow: null,
// supportAndTraining: '',
// employees: null,
// established: null,
// internalListingNumber: null,
// reasonForSale: '',
// brokerLicencing: '',
// internals: '',
// created: null,
// updated: null,
// visits: null,
// lastVisit: null,
// latitude: null,
// longitude: null,
// listingsCategory: 'business',
// };
// }
export function createEmptyBusinessListingCriteria(): BusinessListingCriteria {
return {
start: 0,