Locker module UI: dashboard, class master, locker list, waiting list, allotment form
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
inject
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { of } from 'rxjs';
|
||||
import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/** Custom Services */
|
||||
import { LockerService } from '../../locker.service';
|
||||
|
||||
/** What a pick hands back — enough to show the client and link them. */
|
||||
export interface PickedClient {
|
||||
clientId: number;
|
||||
displayName: string;
|
||||
mobileNo?: string | null;
|
||||
officeName?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client name box with type-ahead (case-insensitive, from 2 letters) over existing clients. Emits the picked client,
|
||||
* and emits null as soon as the text is changed away from it, so a stale client id is never kept for another name.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-locker-client-picker',
|
||||
template: `
|
||||
<mat-form-field appearance="fill" class="picker-field">
|
||||
<mat-label>{{ label | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="nameControl"
|
||||
[matAutocomplete]="auto"
|
||||
[required]="required"
|
||||
autocomplete="off"
|
||||
[readonly]="disabled"
|
||||
/>
|
||||
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayName" (optionSelected)="onSelected($event)">
|
||||
@for (c of suggestions; track c.clientId) {
|
||||
<mat-option [value]="c">
|
||||
<span class="opt-name">{{ c.displayName }}</span>
|
||||
<span class="opt-meta">
|
||||
{{ 'ID' | translate }} {{ c.clientId }}
|
||||
@if (c.mobileNo) {
|
||||
· {{ c.mobileNo }}
|
||||
}
|
||||
@if (c.officeName) {
|
||||
· {{ c.officeName }}
|
||||
}
|
||||
</span>
|
||||
</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@if (pickedId) {
|
||||
<mat-hint class="picked">{{ 'Client ID' | translate }} {{ pickedId }}</mat-hint>
|
||||
} @else if (!disabled) {
|
||||
<mat-hint>{{ 'Type 2+ letters and pick a client' | translate }}</mat-hint>
|
||||
}
|
||||
</mat-form-field>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.picker-field {
|
||||
width: 100%;
|
||||
}
|
||||
.opt-name {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.opt-meta {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: rgb(0 0 0 / 55%);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.picked {
|
||||
color: #1976d2;
|
||||
font-weight: 600;
|
||||
}
|
||||
`
|
||||
],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
ReactiveFormsModule,
|
||||
MatAutocompleteModule
|
||||
]
|
||||
})
|
||||
export class LockerClientPickerComponent implements OnInit, OnChanges {
|
||||
private lockerService = inject(LockerService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
@Input() label = 'Client';
|
||||
@Input() required = false;
|
||||
@Input() disabled = false;
|
||||
/** Pre-selected client, e.g. when an existing application is opened. */
|
||||
@Input() clientId: number | null = null;
|
||||
@Input() clientName: string | null = null;
|
||||
/** Client ids that may not be picked here (e.g. the main applicant in the joint-holder list). */
|
||||
@Input() excludeIds: number[] = [];
|
||||
|
||||
@Output() picked = new EventEmitter<PickedClient | null>();
|
||||
|
||||
nameControl = new FormControl<any>('');
|
||||
suggestions: PickedClient[] = [];
|
||||
pickedId: number | null = null;
|
||||
private pickedName = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.nameControl.valueChanges
|
||||
.pipe(
|
||||
// A picked option puts the object in the box; only typed text counts as a change.
|
||||
filter((value: any): value is string => typeof value === 'string'),
|
||||
debounceTime(300),
|
||||
map((value: string) => value.trim()),
|
||||
distinctUntilChanged(),
|
||||
switchMap((name: string) => {
|
||||
if (this.pickedId && name !== this.pickedName) {
|
||||
this.pickedId = null;
|
||||
this.pickedName = '';
|
||||
this.picked.emit(null);
|
||||
}
|
||||
if (name.length < 2 || this.pickedId) {
|
||||
return of([]);
|
||||
}
|
||||
return this.lockerService.searchClientsByName(name).pipe(catchError(() => of([])));
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe((rows: any[]) => {
|
||||
// The search returns one row per account; keep one per client.
|
||||
const seen = new Set<number>();
|
||||
this.suggestions = (rows ?? [])
|
||||
.filter(
|
||||
(r: any) =>
|
||||
r?.clientId && !this.excludeIds.includes(r.clientId) && !seen.has(r.clientId) && seen.add(r.clientId)
|
||||
)
|
||||
.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['clientId'] || changes['clientName']) {
|
||||
this.pickedId = this.clientId ?? null;
|
||||
this.pickedName = this.clientName ?? '';
|
||||
this.nameControl.setValue(this.pickedName, { emitEvent: false });
|
||||
}
|
||||
}
|
||||
|
||||
onSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
const client: PickedClient = event.option.value;
|
||||
this.pickedId = client.clientId;
|
||||
this.pickedName = client.displayName;
|
||||
this.suggestions = [];
|
||||
this.nameControl.setValue(client.displayName, { emitEvent: false });
|
||||
this.picked.emit(client);
|
||||
}
|
||||
|
||||
displayName = (value: any): string => (typeof value === 'string' ? value : (value?.displayName ?? ''));
|
||||
}
|
||||
Reference in New Issue
Block a user