DDS: approvals tab, vouchers, payout & close, Sundry GL search, closure tab, fixes
This commit is contained in:
@@ -32,6 +32,8 @@ import { ProductsService } from 'app/products/products.service';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { AlertService } from 'app/core/alert/alert.service';
|
||||
import { searchClientsAnyCase } from '../client-search';
|
||||
|
||||
/** Custom Components */
|
||||
import { PrematureCloseDialogComponent } from '../premature-close-dialog/premature-close-dialog.component';
|
||||
@@ -84,6 +86,7 @@ export class CreatePartyComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private organizationService = inject(OrganizationService);
|
||||
private productsService = inject(ProductsService);
|
||||
private alertService = inject(AlertService);
|
||||
private clientsService = inject(ClientsService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dateUtils = inject(Dates);
|
||||
@@ -130,6 +133,11 @@ export class CreatePartyComponent implements OnInit {
|
||||
savingsAccountStatus: string | null = null;
|
||||
savingsAccountBalance: number | null = null;
|
||||
|
||||
/** The DDS account's own number, scheme and balance - the party is the account. */
|
||||
accountNo: string | null = null;
|
||||
accountSchemeName: string | null = null;
|
||||
accountBalance: number | null = null;
|
||||
|
||||
/**
|
||||
* A minor account holder who has come of age. The guardian's authority ended on that day, so the
|
||||
* branch has to take fresh KYC and a new mandate - shown as a call to action rather than a field.
|
||||
@@ -271,9 +279,20 @@ export class CreatePartyComponent implements OnInit {
|
||||
'gender',
|
||||
'birthDate',
|
||||
'telephone',
|
||||
'mobileNoSms'
|
||||
'mobileNoSms',
|
||||
'occupation',
|
||||
'risk',
|
||||
'aadharNo',
|
||||
'panCardNo',
|
||||
'ckycId',
|
||||
'relation',
|
||||
'minorDate',
|
||||
'guardianClientId'
|
||||
];
|
||||
|
||||
/** Fields holding a value from the client record, shown read-only - see fillFromClientRecord. */
|
||||
private filledFromClient = new Set<string>();
|
||||
|
||||
private static readonly MINOR_FIELDS = [
|
||||
'minorDate',
|
||||
'guardianClientId',
|
||||
@@ -323,7 +342,10 @@ export class CreatePartyComponent implements OnInit {
|
||||
'No',
|
||||
Validators.required
|
||||
],
|
||||
aadharNo: ['', Validators.pattern(CreatePartyComponent.AADHAAR_RE)],
|
||||
aadharNo: [
|
||||
'',
|
||||
Validators.pattern(CreatePartyComponent.AADHAAR_RE)
|
||||
],
|
||||
partyName: [
|
||||
'',
|
||||
Validators.required
|
||||
@@ -347,8 +369,14 @@ export class CreatePartyComponent implements OnInit {
|
||||
]
|
||||
],
|
||||
gender: [''],
|
||||
panCardNo: ['', Validators.pattern(CreatePartyComponent.PAN_RE)],
|
||||
telephone: ['', Validators.pattern(CreatePartyComponent.TELEPHONE_RE)],
|
||||
panCardNo: [
|
||||
'',
|
||||
Validators.pattern(CreatePartyComponent.PAN_RE)
|
||||
],
|
||||
telephone: [
|
||||
'',
|
||||
Validators.pattern(CreatePartyComponent.TELEPHONE_RE)
|
||||
],
|
||||
mobileNoSms: [
|
||||
'',
|
||||
[
|
||||
@@ -363,7 +391,10 @@ export class CreatePartyComponent implements OnInit {
|
||||
birthDate: [''],
|
||||
occupation: [''],
|
||||
risk: [''],
|
||||
ckycId: ['', Validators.pattern(CreatePartyComponent.CKYC_RE)],
|
||||
ckycId: [
|
||||
'',
|
||||
Validators.pattern(CreatePartyComponent.CKYC_RE)
|
||||
],
|
||||
adultMinor: [
|
||||
'Adult',
|
||||
Validators.required
|
||||
@@ -511,8 +542,10 @@ export class CreatePartyComponent implements OnInit {
|
||||
this.officesError = this.offices.length === 0 ? 'No branches on file' : '';
|
||||
},
|
||||
error: (error: any) => {
|
||||
this.officesError = error?.error?.defaultUserMessage || error?.error?.developerMessage
|
||||
|| `Could not load branches (${error?.status || 'no response'})`;
|
||||
this.officesError =
|
||||
error?.error?.defaultUserMessage ||
|
||||
error?.error?.developerMessage ||
|
||||
`Could not load branches (${error?.status || 'no response'})`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -547,6 +580,8 @@ export class CreatePartyComponent implements OnInit {
|
||||
for (const controlName of CreatePartyComponent.CLIENT_DERIVED_FIELDS) {
|
||||
this.partyForm.get(controlName)?.setValue('');
|
||||
}
|
||||
this.filledFromClient.clear();
|
||||
this.partyForm.get('adultMinor')?.enable({ emitEvent: false });
|
||||
this.loadedClientName = '';
|
||||
this.clientLookupFailed = false;
|
||||
|
||||
@@ -574,13 +609,9 @@ export class CreatePartyComponent implements OnInit {
|
||||
}
|
||||
|
||||
private searchClients(term: string): void {
|
||||
this.clientsService.getFilteredClients('displayName', 'ASC', false, term).subscribe({
|
||||
next: (response: any) => {
|
||||
const found = response?.pageItems || response || [];
|
||||
this.showingClientList = false;
|
||||
this.clientOptions = Array.isArray(found) ? found.slice(0, 25) : [];
|
||||
},
|
||||
error: () => (this.clientOptions = [])
|
||||
searchClientsAnyCase(this.clientsService, term).subscribe((found: any[]) => {
|
||||
this.showingClientList = false;
|
||||
this.clientOptions = found.slice(0, 25);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -624,13 +655,54 @@ export class CreatePartyComponent implements OnInit {
|
||||
next: (client: any) => {
|
||||
this.loadedClientName = client?.displayName || knownName || '';
|
||||
this.clientLookupFailed = false;
|
||||
this.setField('partyName', client?.displayName);
|
||||
this.setField('mobileNoSms', client?.mobileNo);
|
||||
this.setField('gender', client?.gender?.name || client?.gender);
|
||||
this.fillFromClientRecord('partyName', client?.displayName);
|
||||
this.fillFromClientRecord('mobileNoSms', client?.mobileNo);
|
||||
this.fillFromClientRecord('telephone', client?.alternateMobileNo);
|
||||
// Alerts go where the client can actually be reached: SMS with a mobile on record, email with
|
||||
// an email address on record.
|
||||
this.partyForm.get('isSmsActive')?.setValue(!!client?.mobileNo);
|
||||
this.partyForm.get('isEmailActive')?.setValue(!!client?.emailAddress);
|
||||
// The client record says FEMALE; this form's list says Female.
|
||||
this.fillFromClientRecord('gender', titleCase(client?.gender?.name || client?.gender));
|
||||
const dateOfBirth = this.toDate(client?.dateOfBirth);
|
||||
if (dateOfBirth) {
|
||||
this.partyForm.get('birthDate')?.setValue(dateOfBirth);
|
||||
this.filledFromClient.add('birthDate');
|
||||
}
|
||||
// The client's own occupation, offered even when this form's short list does not carry it.
|
||||
const occupation = client?.occupation?.name || client?.occupationCategory?.name;
|
||||
if (occupation) {
|
||||
if (!this.occupations.includes(occupation)) {
|
||||
this.occupations = [
|
||||
...this.occupations,
|
||||
occupation
|
||||
];
|
||||
}
|
||||
this.fillFromClientRecord('occupation', occupation);
|
||||
}
|
||||
// A minor on the client record is a minor account here, with the same guardian.
|
||||
this.partyForm.get('adultMinor')?.setValue(client?.isMinor ? 'Minor' : 'Adult');
|
||||
// Adult or minor is the client record's, checked when the client was opened - not changed here.
|
||||
this.partyForm.get('adultMinor')?.disable({ emitEvent: false });
|
||||
this.filledFromClient.add('adultMinor');
|
||||
if (client?.isMinor) {
|
||||
if (dateOfBirth) {
|
||||
// The day they come of age - eighteen is majority in India.
|
||||
const majority = new Date(dateOfBirth);
|
||||
majority.setFullYear(majority.getFullYear() + 18);
|
||||
this.partyForm.get('minorDate')?.setValue(majority);
|
||||
this.filledFromClient.add('minorDate');
|
||||
}
|
||||
if (client?.guardianClientId) {
|
||||
this.partyForm
|
||||
.get('guardianClientId')
|
||||
?.setValue({ id: client.guardianClientId, displayName: client.guardianClientDisplayName || '' });
|
||||
this.filledFromClient.add('guardianClientId');
|
||||
}
|
||||
this.fillFromClientRecord('relation', titleCase(client?.guardianRelation));
|
||||
}
|
||||
this.loadClientRisk(String(clientId));
|
||||
// Nominees are not brought across: the officer records them for this account.
|
||||
// Proceeds usually go back to an account at the customer's own branch, so that is offered
|
||||
// first - any branch can still be picked, since the account may well be at another.
|
||||
const creditToBranch = this.partyForm.get('creditToBranch');
|
||||
@@ -692,11 +764,11 @@ export class CreatePartyComponent implements OnInit {
|
||||
continue;
|
||||
}
|
||||
if (words.includes('aadhaar') || words.includes('aadhar')) {
|
||||
this.setField('aadharNo', documentKey);
|
||||
this.fillFromClientRecord('aadharNo', documentKey);
|
||||
} else if (words.includes('pan')) {
|
||||
this.setField('panCardNo', documentKey);
|
||||
this.fillFromClientRecord('panCardNo', documentKey);
|
||||
} else if (words.includes('ckyc')) {
|
||||
this.setField('ckycId', documentKey);
|
||||
this.fillFromClientRecord('ckycId', documentKey);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -713,6 +785,39 @@ export class CreatePartyComponent implements OnInit {
|
||||
return typeof value === 'object' ? `${value.id} — ${value.displayName}` : String(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a value from the client record, which was checked when the client was opened, and locks
|
||||
* the field so it is not typed - and checked - a second time. A value the client does not have is
|
||||
* left for the officer to enter.
|
||||
*/
|
||||
private fillFromClientRecord(controlName: string, value: any): void {
|
||||
if (value === null || value === undefined || String(value).trim() === '') {
|
||||
return;
|
||||
}
|
||||
this.setField(controlName, value);
|
||||
this.filledFromClient.add(controlName);
|
||||
}
|
||||
|
||||
/** Whether a field holds a value taken from the client record, and so is shown read-only. */
|
||||
isFromClient(controlName: string): boolean {
|
||||
return this.filledFromClient.has(controlName);
|
||||
}
|
||||
|
||||
/** The client's risk category, from their KYC record. */
|
||||
private loadClientRisk(clientId: string): void {
|
||||
this.clientsService.getClientKyc(clientId, true).subscribe({
|
||||
next: (kyc: any) => {
|
||||
// kycRiskCategory.medium -> Medium
|
||||
const code = String(kyc?.riskCategoryCode || '')
|
||||
.split('.')
|
||||
.pop();
|
||||
this.fillFromClientRecord('risk', titleCase(code));
|
||||
},
|
||||
// A client without a KYC record just leaves the risk to be chosen.
|
||||
error: () => undefined
|
||||
});
|
||||
}
|
||||
|
||||
private loadClientAddress(clientId: string): void {
|
||||
this.clientsService.getClientAddressData(clientId, true).subscribe({
|
||||
next: (addresses: any) => {
|
||||
@@ -720,11 +825,11 @@ export class CreatePartyComponent implements OnInit {
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
this.setField('address1', address.addressLine1 || address.street);
|
||||
this.setField('address2', address.addressLine2);
|
||||
this.setField('address3', address.addressLine3);
|
||||
this.setField('city', address.city);
|
||||
this.setField('pincode', address.postalCode);
|
||||
this.fillFromClientRecord('address1', address.addressLine1 || address.street);
|
||||
this.fillFromClientRecord('address2', address.addressLine2);
|
||||
this.fillFromClientRecord('address3', address.addressLine3);
|
||||
this.fillFromClientRecord('city', address.city);
|
||||
this.fillFromClientRecord('pincode', address.postalCode);
|
||||
},
|
||||
// A client with no address on file is normal - the officer just types it.
|
||||
error: () => undefined
|
||||
@@ -802,8 +907,12 @@ export class CreatePartyComponent implements OnInit {
|
||||
private filterAgents(term: string): void {
|
||||
const needle = term.toLowerCase();
|
||||
const match = (agent: any) =>
|
||||
String(agent.agentCode || '').toLowerCase().includes(needle) ||
|
||||
String(agent.agentNameId || '').toLowerCase().includes(needle) ||
|
||||
String(agent.agentCode || '')
|
||||
.toLowerCase()
|
||||
.includes(needle) ||
|
||||
String(agent.agentNameId || '')
|
||||
.toLowerCase()
|
||||
.includes(needle) ||
|
||||
String(agent.id) === term;
|
||||
|
||||
if (this.allAgents.length > 0) {
|
||||
@@ -868,14 +977,28 @@ export class CreatePartyComponent implements OnInit {
|
||||
*/
|
||||
private wireSubTypeSearch(): void {
|
||||
this.partyForm.get('subType')?.valueChanges.subscribe((value) => {
|
||||
const term = String(value ?? '').toLowerCase().trim();
|
||||
// A new account takes its product's rate, which the maturity preview is worked out on. The
|
||||
// server sets the same rate when it saves; an account being edited already has it.
|
||||
const scheme = this.subTypes.find((type) => type.accountTypeCode === value);
|
||||
if (scheme && !this.partyId) {
|
||||
this.partyForm.get('interestRate')?.setValue(Number(scheme.interestRate) || 0);
|
||||
}
|
||||
const term = String(value ?? '')
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
this.subTypeOptions = !term
|
||||
? this.subTypes
|
||||
: this.subTypes.filter(
|
||||
(type) =>
|
||||
String(type.accountTypeCode || '').toLowerCase().includes(term) ||
|
||||
String(type.accountTypeDescription || '').toLowerCase().includes(term) ||
|
||||
String(type.accountTypeGroup || '').toLowerCase().includes(term)
|
||||
String(type.accountTypeCode || '')
|
||||
.toLowerCase()
|
||||
.includes(term) ||
|
||||
String(type.accountTypeDescription || '')
|
||||
.toLowerCase()
|
||||
.includes(term) ||
|
||||
String(type.accountTypeGroup || '')
|
||||
.toLowerCase()
|
||||
.includes(term)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -970,8 +1093,6 @@ export class CreatePartyComponent implements OnInit {
|
||||
return this.partyForm.get('nomineeApplicable')?.value === 'Yes';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Which entry in the list a stored reason belongs to.
|
||||
*
|
||||
@@ -1051,7 +1172,10 @@ export class CreatePartyComponent implements OnInit {
|
||||
*/
|
||||
private wireMaturityCalculation(): void {
|
||||
// The term drives everything downstream, so changing it refills Days as well.
|
||||
for (const controlName of ['openingDate', 'month']) {
|
||||
for (const controlName of [
|
||||
'openingDate',
|
||||
'month'
|
||||
]) {
|
||||
this.partyForm.get(controlName)?.valueChanges.subscribe(() => this.recalculateTerm());
|
||||
}
|
||||
// Days typed by hand stands: it only moves the maturity date when no month was given.
|
||||
@@ -1059,7 +1183,10 @@ export class CreatePartyComponent implements OnInit {
|
||||
this.recalculateMaturityDate();
|
||||
this.recalculateMaturityAmount();
|
||||
});
|
||||
for (const controlName of ['dailyDepositAmount', 'interestRate']) {
|
||||
for (const controlName of [
|
||||
'dailyDepositAmount',
|
||||
'interestRate'
|
||||
]) {
|
||||
this.partyForm.get(controlName)?.valueChanges.subscribe(() => this.recalculateMaturityAmount());
|
||||
}
|
||||
}
|
||||
@@ -1131,7 +1258,11 @@ export class CreatePartyComponent implements OnInit {
|
||||
this.partyForm
|
||||
.get('maturityAmount')
|
||||
?.setValue(
|
||||
this.maturityAmountOf(Number(raw.dailyDepositAmount) || 0, Number(raw.days) || 0, Number(raw.interestRate) || 0),
|
||||
this.maturityAmountOf(
|
||||
Number(raw.dailyDepositAmount) || 0,
|
||||
Number(raw.days) || 0,
|
||||
Number(raw.interestRate) || 0
|
||||
),
|
||||
{ emitEvent: false }
|
||||
);
|
||||
}
|
||||
@@ -1372,6 +1503,9 @@ export class CreatePartyComponent implements OnInit {
|
||||
this.savingsProductName = null;
|
||||
this.savingsAccountStatus = null;
|
||||
this.savingsAccountBalance = null;
|
||||
this.accountNo = null;
|
||||
this.accountSchemeName = null;
|
||||
this.accountBalance = null;
|
||||
this.majorOn = null;
|
||||
this.minorTurnedMajor = false;
|
||||
this.loadedClientName = '';
|
||||
@@ -1451,7 +1585,7 @@ export class CreatePartyComponent implements OnInit {
|
||||
dailyDepositAmount: raw.dailyDepositAmount,
|
||||
days: raw.days,
|
||||
ledgerNo: raw.ledgerNo || undefined,
|
||||
interestRate: raw.interestRate || 0,
|
||||
// No interest rate: the server gives the account its product's rate.
|
||||
withdrawType: raw.withdrawType || undefined,
|
||||
lien: raw.lien === 'Yes',
|
||||
// Sent only when the money is actually held. Releasing it clears the reason on the
|
||||
@@ -1489,10 +1623,24 @@ export class CreatePartyComponent implements OnInit {
|
||||
}))
|
||||
};
|
||||
|
||||
const partyLabel = raw.partyCode || raw.partyName;
|
||||
const done = (response: any) => {
|
||||
// Under maker-checker the command is only recorded, and nothing has been saved yet.
|
||||
if (!!response?.commandId && response?.resourceId == null) {
|
||||
this.alertService.alert({
|
||||
type: 'Sent for Approval',
|
||||
message: this.partyId
|
||||
? `Changes to party ${partyLabel} were sent for a manager's approval and will take effect once approved.`
|
||||
: `New party ${partyLabel} was sent for a manager's approval and will be created once approved.`
|
||||
});
|
||||
}
|
||||
this.exit();
|
||||
};
|
||||
|
||||
if (this.partyId) {
|
||||
this.organizationService.updateParty(this.partyId, payload).subscribe(() => this.exit());
|
||||
this.organizationService.updateParty(this.partyId, payload).subscribe(done);
|
||||
} else {
|
||||
this.organizationService.createParty(payload).subscribe(() => this.exit());
|
||||
this.organizationService.createParty(payload).subscribe(done);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1504,7 +1652,7 @@ export class CreatePartyComponent implements OnInit {
|
||||
const locale = this.settingsService.language.code;
|
||||
this.organizationService
|
||||
.closeParty(this.partyId, {
|
||||
closeDate: this.dateUtils.formatDate(new Date(), dateFormat),
|
||||
closeDate: this.dateUtils.formatDate(this.settingsService.businessDate || new Date(), dateFormat),
|
||||
dateFormat,
|
||||
locale
|
||||
})
|
||||
@@ -1533,7 +1681,12 @@ export class CreatePartyComponent implements OnInit {
|
||||
|
||||
private get agentCodeOfForm(): string {
|
||||
const value = this.partyForm.get('agentId')?.value;
|
||||
return value && typeof value === 'object' ? value.agentCode : String(value ?? '');
|
||||
if (value && typeof value === 'object') {
|
||||
return value.agentCode;
|
||||
}
|
||||
// The form holds the agent's id; the dialog names them by code.
|
||||
const agent = (this.allAgents || []).find((option: any) => String(option.id) === String(value));
|
||||
return agent?.agentCode ?? String(value ?? '');
|
||||
}
|
||||
|
||||
get canCloseEarly(): boolean {
|
||||
@@ -1562,6 +1715,7 @@ export class CreatePartyComponent implements OnInit {
|
||||
interestRate: this.partyForm.get('interestRate')?.value,
|
||||
maturityDate: this.partyForm.get('maturityDate')?.value,
|
||||
currentBalance: this.partyForm.get('currentBalance')?.value,
|
||||
openingDate: this.partyForm.get('openingDate')?.value,
|
||||
clientId: this.clientIdOfForm,
|
||||
agentCode: this.agentCodeOfForm
|
||||
},
|
||||
@@ -1573,20 +1727,35 @@ export class CreatePartyComponent implements OnInit {
|
||||
if (!closeCommand) {
|
||||
return;
|
||||
}
|
||||
const partyCode = this.partyForm.get('partyCode')?.value;
|
||||
// Inside the lock-in it is a request with a reason, on its own permission, for a manager to pass.
|
||||
const withinLockin = !!closeCommand.lockinOverrideReason;
|
||||
// The date goes up the way every other date on this system does - formatted to the user's own
|
||||
// setting, with that setting and their locale alongside so the server reads it back the same
|
||||
// way. An ordinary close sends it like this too; two ways of closing an account should not
|
||||
// disagree about something as plain as what day it is.
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const locale = this.settingsService.language.code;
|
||||
this.organizationService
|
||||
.prematureCloseParty(this.partyId as number, {
|
||||
...closeCommand,
|
||||
closeDate: this.dateUtils.formatDate(new Date(), dateFormat),
|
||||
dateFormat,
|
||||
locale
|
||||
})
|
||||
.subscribe(() => this.exit());
|
||||
const request = {
|
||||
...closeCommand,
|
||||
closeDate: this.dateUtils.formatDate(this.settingsService.businessDate || new Date(), dateFormat),
|
||||
dateFormat,
|
||||
locale
|
||||
};
|
||||
(withinLockin
|
||||
? this.organizationService.prematureCloseOverrideParty(this.partyId as number, request)
|
||||
: this.organizationService.prematureCloseParty(this.partyId as number, request)
|
||||
).subscribe((response: any) => {
|
||||
// Under maker-checker the command is only recorded, and nothing has closed yet.
|
||||
const awaitingApproval = !!response?.commandId && response?.resourceId == null;
|
||||
this.alertService.alert({
|
||||
type: awaitingApproval ? 'Sent for Approval' : 'Party Closed',
|
||||
message: awaitingApproval
|
||||
? `Early closure of ${partyCode} is waiting for a manager's approval. The account stays open until then.`
|
||||
: `${partyCode} is closed.`
|
||||
});
|
||||
this.exit();
|
||||
});
|
||||
// A refusal - inside the lock-in, held under lien, KYC outstanding - is shown by the platform's
|
||||
// own error handler, which names the reason. Repeating it here would say the same thing twice.
|
||||
});
|
||||
@@ -1662,6 +1831,9 @@ export class CreatePartyComponent implements OnInit {
|
||||
this.savingsProductName = party.savingsProductName || null;
|
||||
this.savingsAccountStatus = party.savingsAccountStatus || null;
|
||||
this.savingsAccountBalance = party.savingsAccountBalance ?? null;
|
||||
this.accountNo = party.accountNo || null;
|
||||
this.accountSchemeName = party.subTypeDescription || party.subType || null;
|
||||
this.accountBalance = party.currentBalance ?? null;
|
||||
this.majorOn = party.majorOn || null;
|
||||
this.minorTurnedMajor = party.minorTurnedMajor === true;
|
||||
this.loadedClientName = party.clientName || '';
|
||||
@@ -1762,3 +1934,11 @@ export class CreatePartyComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** FEMALE or female -> Female; each word, so "SELF EMPLOYED" -> "Self Employed". */
|
||||
function titleCase(value: any): string {
|
||||
return String(value ?? '')
|
||||
.toLowerCase()
|
||||
.replace(/(^|\s)(\S)/g, (_match, space, letter) => space + letter.toUpperCase())
|
||||
.trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user