diff --git a/src/app/admin-view/key/key.component.html b/src/app/admin-view/key/key.component.html index 85fb7aa..200618d 100644 --- a/src/app/admin-view/key/key.component.html +++ b/src/app/admin-view/key/key.component.html @@ -8,7 +8,7 @@ - +
diff --git a/src/app/admin-view/key/new-key/new-key.component.html b/src/app/admin-view/key/new-key/new-key.component.html index 56d3310..c1ef8ad 100644 --- a/src/app/admin-view/key/new-key/new-key.component.html +++ b/src/app/admin-view/key/new-key/new-key.component.html @@ -1,6 +1,6 @@
- + Sala @for (item of rooms; track $index) { @@ -9,17 +9,9 @@ Wymagane - + Wypożyczający - - - - Zła nazwa użytkownika + Wymagane diff --git a/src/app/admin-view/key/new-key/new-key.component.ts b/src/app/admin-view/key/new-key/new-key.component.ts index 4d02e89..f9969fb 100644 --- a/src/app/admin-view/key/new-key/new-key.component.ts +++ b/src/app/admin-view/key/new-key/new-key.component.ts @@ -1,8 +1,8 @@ -import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { AdminCommService } from '../../admin-comm.service'; import { MatDialogRef } from '@angular/material/dialog'; import { FormControl, FormGroup } from '@angular/forms'; -import { startWith } from 'rxjs'; +import { UserSearchResult } from 'src/app/commonComponents/user-search/user-search.component'; @Component({ selector: 'app-new-key', @@ -10,11 +10,10 @@ import { startWith } from 'rxjs'; styleUrl: './new-key.component.scss' }) export class NewKeyComponent implements OnInit { - // @ViewChild('input') input!: ElementRef rooms: string[] = [] form = new FormGroup({ room: new FormControl(""), - user: new FormControl("") + user: new FormControl(null) }) unames: any[] = [] constructor ( private ac: AdminCommService, public dialogRef: MatDialogRef ) {} @@ -24,26 +23,11 @@ export class NewKeyComponent implements OnInit { this.rooms = v }) } - - // filter() { - // const v = this.input.nativeElement.value - // console.log(v); - - // if (v) { - // this.ac.userFilter(v.toLowerCase()).subscribe((v) => { - // this.unames = v - // }) - // } else { - // this.unames = [] - // } - // } send() { if (this.form.valid) { this.dialogRef.close(this.form.value) - } else { - this.form.controls['user'].setErrors({unf: true}) - } + } } } diff --git a/src/app/admin-view/notifications/notifications.component.html b/src/app/admin-view/notifications/notifications.component.html index f4720b7..c5fec6a 100644 --- a/src/app/admin-view/notifications/notifications.component.html +++ b/src/app/admin-view/notifications/notifications.component.html @@ -1,10 +1,10 @@
- + - Nazwa użytkownika - + Użytkownik + diff --git a/src/app/admin-view/notifications/notifications.component.ts b/src/app/admin-view/notifications/notifications.component.ts index 1335e9b..9bc99f9 100644 --- a/src/app/admin-view/notifications/notifications.component.ts +++ b/src/app/admin-view/notifications/notifications.component.ts @@ -6,6 +6,7 @@ import { Group } from 'src/app/types/group'; import { LocalStorageService } from 'src/app/services/local-storage.service'; import { ToolbarService } from '../toolbar/toolbar.service'; import { ActivatedRoute, Router } from '@angular/router'; +import { UserSearchResult } from 'src/app/commonComponents/user-search/user-search.component'; @Component({ selector: 'app-notifications', @@ -44,9 +45,9 @@ export class NotificationsComponent implements OnInit, OnDestroy { success?: { sent: number; possible: number; }; - form = new FormGroup({ + form = new FormGroup({ recp: new FormGroup({ - uname: new FormControl(''), + uid: new FormControl(null), room: new FormControl(null), group: new FormControl(''), type: new FormControl<"room" | "uname" | "group">('uname', {nonNullable: true}) @@ -56,19 +57,8 @@ export class NotificationsComponent implements OnInit, OnDestroy { }) submit() { - this.acs.notif.send(this.form.value as Notification).subscribe((data) => { + this.acs.notif.send({...this.form.value, recp: {...this.form.get("recp")?.value, uid: this.form.controls['recp'].controls['uid'].value?._id}} as Notification).subscribe((data) => { this.success = data }) } } - -interface NotificationForm { - body: FormControl; - title: FormControl; - recp: FormGroup<{ - uname: FormControl; - room: FormControl; - group: FormControl; - type: FormControl<"room" | "uname" | "group">; - }> -} \ No newline at end of file diff --git a/src/app/commonComponents/user-search/user-search.component.html b/src/app/commonComponents/user-search/user-search.component.html index 5a738d6..298bdbf 100644 --- a/src/app/commonComponents/user-search/user-search.component.html +++ b/src/app/commonComponents/user-search/user-search.component.html @@ -1,16 +1,15 @@ - - {{label}} - - - - - @for (item of list; track $index) { - - @if (item.fname) { - {{item.fname}} {{item.surname}} ({{item.room}}) - } @else { - {{item.uname}} - } - - } - \ No newline at end of file +
+ + + + @for (item of list; track $index) { + + @if (item.fname) { + {{item.fname}} {{item.surname}} ({{item.room}}) + } @else { + {{item.uname}} + } + + } + +
\ No newline at end of file diff --git a/src/app/commonComponents/user-search/user-search.component.scss b/src/app/commonComponents/user-search/user-search.component.scss index f06ba3a..0313682 100644 --- a/src/app/commonComponents/user-search/user-search.component.scss +++ b/src/app/commonComponents/user-search/user-search.component.scss @@ -6,4 +6,13 @@ .room { color: gray; +} + +input { + border: none; + background: none; + padding: 0; + outline: 0; + font: inherit; + color: currentColor; } \ No newline at end of file diff --git a/src/app/commonComponents/user-search/user-search.component.ts b/src/app/commonComponents/user-search/user-search.component.ts index eaa9dcc..3be722f 100644 --- a/src/app/commonComponents/user-search/user-search.component.ts +++ b/src/app/commonComponents/user-search/user-search.component.ts @@ -1,10 +1,12 @@ -import { Component, EventEmitter, forwardRef, Input, Output, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Component, DoCheck, ElementRef, HostBinding, Input, OnDestroy, Optional, Self } from '@angular/core'; +import { ControlValueAccessor, FormControl, FormGroupDirective, NgControl, NgForm } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; -import { MatInput } from '@angular/material/input'; +import { MatFormFieldControl } from '@angular/material/form-field'; +import { Subject } from 'rxjs'; import { AdminCommService } from 'src/app/admin-view/admin-comm.service'; -interface UserSearchResult { +export interface UserSearchResult { _id: string; fname: string; surname: string; @@ -18,26 +20,120 @@ interface UserSearchResult { styleUrl: './user-search.component.scss', providers: [ { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => UserSearchComponent), - multi: true + provide: MatFormFieldControl, + useExisting: UserSearchComponent } ], host: { '(blur)': '_onTouched()' } }) -export class UserSearchComponent implements ControlValueAccessor { +export class UserSearchComponent implements ControlValueAccessor, MatFormFieldControl, OnDestroy, DoCheck { protected loading: boolean = false - @Input() label?: boolean control: FormControl = new FormControl(); protected list: UserSearchResult[] = [] private timeout?: NodeJS.Timeout private _onChange!: (_: UserSearchResult) => void private _onTouched!: any - constructor(readonly acu: AdminCommService) { + static nextId = 0; + + @Input() + public get value(): UserSearchResult | null { + return this.control.value; + } + + public set value(value: UserSearchResult | null) { + this.control.setValue(value) + this.stateChanges.next() + } + + touched = false + + stateChanges = new Subject(); + + @HostBinding() id: string = `app-user-search-${UserSearchComponent.nextId++}`; + + private _placeholder: string = ""; + @Input() + public get placeholder(): string { + return this._placeholder; + } + public set placeholder(value: string) { + this._placeholder = value; + this.stateChanges.next() + } + + focused: boolean = false; + onFocusIn(event: FocusEvent) { + if (!this.focused) { + this.focused = true; + this.stateChanges.next(); + } + } + + onFocusOut(event: FocusEvent) { + if (!this._elementRef.nativeElement.contains(event.relatedTarget as Element)) { + this.touched = true + this.focused = false; + this._onTouched(); + this.stateChanges.next(); + } + } + get empty(): boolean { + return !this.control.value + } + @HostBinding('class.floating') + get shouldLabelFloat(): boolean { + return this.focused || !this.empty + } + private _required: boolean = false; + @Input() + public get required(): boolean { + return this._required; + } + + public set required(value: BooleanInput) { + this._required = coerceBooleanProperty(value); + this.stateChanges.next() + } + + private _disabled: boolean = false; + @Input() + public get disabled(): boolean { + return this._disabled; + } + public set disabled(value: BooleanInput) { + this._disabled = coerceBooleanProperty(value); + this._disabled ? this.control.disable() : this.control.enable() + this.stateChanges.next() + } + errorState: boolean = false + controlType?: string | undefined = "app-user-search"; + autofilled?: boolean | undefined; + @Input('aria-describedby') userAriaDescribedBy?: string; + setDescribedByIds(ids: string[]): void { + const controlElement = this._elementRef.nativeElement.querySelector('.app-user-search-container')!; + controlElement.setAttribute('aria-describedby', ids.join(' ')) + } + onContainerClick(event: MouseEvent): void { + if ((event.target as Element).tagName.toLowerCase() != 'input') { + this._elementRef.nativeElement.querySelector('input').focus() + } + } + + constructor( + readonly acu: AdminCommService, + @Optional() @Self() public ngControl: NgControl, + @Optional() private _parentForm: NgForm, + @Optional() private _parentFormGroup: FormGroupDirective, + private _elementRef: ElementRef + ) { + if (this.ngControl != null) { + (this.ngControl as NgControl).valueAccessor = this + } this.control.valueChanges.subscribe(() => { + if (typeof this.control.value == "object") return; this.loading = true if (this.timeout) clearTimeout(this.timeout) this.timeout = setTimeout(() => { @@ -48,9 +144,28 @@ export class UserSearchComponent implements ControlValueAccessor { }, 500) }) } + ngDoCheck(): void { + if (this.ngControl) { + this.updateErrorState() + } + } + private updateErrorState() { + const parent = this._parentFormGroup || this._parentForm - writeValue(obj: string): void { - this.control.setValue(obj) + const oldState = this.errorState; + const newState = (this.ngControl?.invalid || this.control.invalid) && (this.touched || parent.submitted); + + if (oldState !== newState) { + this.errorState = newState + this.stateChanges.next() + } + } + ngOnDestroy(): void { + this.stateChanges.complete() + } + + writeValue(obj: UserSearchResult): void { + this.value = obj } registerOnChange(fn: (_: UserSearchResult) => void): void { @@ -62,7 +177,7 @@ export class UserSearchComponent implements ControlValueAccessor { } setDisabledState?(isDisabled: boolean): void { - isDisabled ? this.control.disable() : this.control.enable() + this.disabled = isDisabled } protected displayFn(u: UserSearchResult): string { @@ -71,6 +186,8 @@ export class UserSearchComponent implements ControlValueAccessor { } protected saveValue(e: MatAutocompleteSelectedEvent) { - this._onChange(this.control.value) + this.autofilled = true + this.value = e.option.value + this._onChange(this.value!) } } diff --git a/src/app/types/notification.ts b/src/app/types/notification.ts index b654ee0..62c8524 100644 --- a/src/app/types/notification.ts +++ b/src/app/types/notification.ts @@ -2,7 +2,7 @@ export interface Notification { body: string; title: string; recp: { - uname: string | null; + uid: string | null; room: string | null; type: "all" | "room" | "uname" }
Sala